@codyswann/lisa 4.70.1 → 4.70.3
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/README.md +18 -0
- package/all/copy-overwrite/scripts/lib/history-secret-scanner.mjs +302 -5
- package/all/copy-overwrite/scripts/lisa-work-item.mjs +71 -10
- package/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +16 -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 +7 -4
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +3 -0
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/prune-retired-entire-hooks.d.ts +25 -0
- package/dist/migrations/prune-retired-entire-hooks.d.ts.map +1 -0
- package/dist/migrations/prune-retired-entire-hooks.js +146 -0
- package/dist/migrations/prune-retired-entire-hooks.js.map +1 -0
- package/package.json +4 -4
- package/plugins/lisa/.claude-plugin/plugin.json +1 -64
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- 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-cursor/.claude-plugin/plugin.json +1 -1
- 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/.claude-plugin/plugin.json +0 -63
- package/rails/merge/.claude/settings.json +0 -47
- package/scripts/install-claude-plugins.sh +1 -1
package/README.md
CHANGED
|
@@ -259,6 +259,24 @@ servers, or configuration. Other harnesses retain their existing delivery
|
|
|
259
259
|
behavior. For a new project, run the CLI ephemerally with
|
|
260
260
|
`bunx @codyswann/lisa setup-project ...`.
|
|
261
261
|
|
|
262
|
+
Entire session capture is host-owned opt-in. Lisa's Rails settings and base
|
|
263
|
+
Claude plugin register no Entire commands. Full apply also removes the seven
|
|
264
|
+
exact automatic command strings shipped by older Lisa releases, because an
|
|
265
|
+
array-union merge cannot retire installed entries by itself. Other hooks and
|
|
266
|
+
settings survive, and Lisa does not inspect or delete existing session data.
|
|
267
|
+
This does not change user/global settings or registrations owned by other
|
|
268
|
+
plugins.
|
|
269
|
+
|
|
270
|
+
To opt in, review capture, retention, and access first, then create a host-owned
|
|
271
|
+
wrapper such as `scripts/private-session-hooks.sh`. Register its distinct
|
|
272
|
+
command in Claude's project hook settings, for example
|
|
273
|
+
`bash "$CLAUDE_PROJECT_DIR/scripts/private-session-hooks.sh" session-start`.
|
|
274
|
+
Lisa preserves that wrapper on reapply. Reusing an exact retired Lisa command
|
|
275
|
+
does not establish host ownership and the migration will remove it. Keep the
|
|
276
|
+
Lisa governance hooks enabled; no Entire configuration flag is required or
|
|
277
|
+
interpreted by Lisa. Start a fresh session after updating the installed plugin
|
|
278
|
+
so it loads the current registrations.
|
|
279
|
+
|
|
262
280
|
Successful starter creation records the repository, tracked branch and copied
|
|
263
281
|
revision in `starter.templates`. Git clones record their own head before setup
|
|
264
282
|
replaces the clone history; GitHub template creation checks the copied tree
|
|
@@ -222,6 +222,301 @@ export const qualifyScanner = requested => {
|
|
|
222
222
|
return binary;
|
|
223
223
|
};
|
|
224
224
|
|
|
225
|
+
const EVIDENCE_BUDGET = 64 * 1024 * 1024;
|
|
226
|
+
const NO_REPLACEMENTS = "--no-replace-objects";
|
|
227
|
+
const TAXONOMY = "Disk/missing/corrupt";
|
|
228
|
+
const NARRATIVE = [
|
|
229
|
+
"AccessDenied boundaries,",
|
|
230
|
+
`${TAXONOMY} falsifications.`,
|
|
231
|
+
].join(" ");
|
|
232
|
+
const safePath = value =>
|
|
233
|
+
typeof value === "string" &&
|
|
234
|
+
value.length > 0 &&
|
|
235
|
+
!/[\\\u0000-\u001f\u007f]/u.test(value) &&
|
|
236
|
+
!/^(?:\/|[A-Za-z]:)/u.test(value) &&
|
|
237
|
+
value.split("/").every(part => part && part !== "." && part !== "..");
|
|
238
|
+
|
|
239
|
+
/** Read authentic regular Git blobs, never trimmed text or worktree evidence. */
|
|
240
|
+
const readBlob = (file, commit, cwd, budget) => {
|
|
241
|
+
if (!safePath(file)) return null;
|
|
242
|
+
const entries = gitRead(
|
|
243
|
+
["--literal-pathspecs", "ls-tree", "-z", commit, "--", file],
|
|
244
|
+
cwd
|
|
245
|
+
)
|
|
246
|
+
.split("\0")
|
|
247
|
+
.filter(Boolean);
|
|
248
|
+
if (entries.length !== 1) return null;
|
|
249
|
+
const entry = entries[0].match(
|
|
250
|
+
/^(100644|100755) blob ([a-f0-9]{40}|[a-f0-9]{64})\t([\s\S]*)$/u
|
|
251
|
+
);
|
|
252
|
+
if (!entry || entry[3] !== file || entry[2].length !== commit.length)
|
|
253
|
+
return null;
|
|
254
|
+
const sizeText = gitRead(["cat-file", "-s", entry[2]], cwd);
|
|
255
|
+
const size = Number(sizeText);
|
|
256
|
+
if (!/^\d+$/u.test(sizeText) || !Number.isSafeInteger(size))
|
|
257
|
+
throw new HistorySecretError(
|
|
258
|
+
"Git blob size is invalid. Repair complete Git objects and retry; safety is unproved."
|
|
259
|
+
);
|
|
260
|
+
if (size > budget.remaining) return null;
|
|
261
|
+
const result = spawnSync(
|
|
262
|
+
"git",
|
|
263
|
+
[NO_REPLACEMENTS, "cat-file", "blob", entry[2]],
|
|
264
|
+
{
|
|
265
|
+
cwd,
|
|
266
|
+
env: gitEnvironment(),
|
|
267
|
+
timeout: 120000,
|
|
268
|
+
maxBuffer: EVIDENCE_BUDGET,
|
|
269
|
+
}
|
|
270
|
+
);
|
|
271
|
+
if (
|
|
272
|
+
result.error ||
|
|
273
|
+
result.signal ||
|
|
274
|
+
result.status !== 0 ||
|
|
275
|
+
result.stdout.length !== size ||
|
|
276
|
+
createHash(commit.length === 40 ? "sha1" : "sha256")
|
|
277
|
+
.update(`blob ${size}\0`)
|
|
278
|
+
.update(result.stdout)
|
|
279
|
+
.digest("hex") !== entry[2]
|
|
280
|
+
)
|
|
281
|
+
throw new HistorySecretError(
|
|
282
|
+
"Git blob bytes cannot be verified. Repair complete Git objects and retry; no raw source is published."
|
|
283
|
+
);
|
|
284
|
+
budget.remaining -= size;
|
|
285
|
+
return result.stdout;
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/** Native JSON validation plus bounded duplicate-aware lexical role attribution. */
|
|
289
|
+
const evidenceMap = bytes => {
|
|
290
|
+
if (bytes.length > 1024 * 1024) return null;
|
|
291
|
+
const text = bytes.toString("utf8");
|
|
292
|
+
if (!Buffer.from(text).equals(bytes)) return null;
|
|
293
|
+
try {
|
|
294
|
+
const value = JSON.parse(text);
|
|
295
|
+
if (
|
|
296
|
+
!value ||
|
|
297
|
+
typeof value !== "object" ||
|
|
298
|
+
Array.isArray(value) ||
|
|
299
|
+
!value.source_sha256 ||
|
|
300
|
+
typeof value.source_sha256 !== "object" ||
|
|
301
|
+
Array.isArray(value.source_sha256)
|
|
302
|
+
)
|
|
303
|
+
return null;
|
|
304
|
+
const entries = Object.entries(value.source_sha256);
|
|
305
|
+
if (
|
|
306
|
+
!entries.length ||
|
|
307
|
+
entries.length > 256 ||
|
|
308
|
+
entries.some(
|
|
309
|
+
([file, hash]) =>
|
|
310
|
+
!safePath(file) ||
|
|
311
|
+
typeof hash !== "string" ||
|
|
312
|
+
!/^[a-f0-9]{64}$/u.test(hash)
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
return null;
|
|
316
|
+
const tokens = [
|
|
317
|
+
...text.matchAll(/"(?:\\[\s\S]|[^"\\])*"|[{}[\],:]|[^{}[\],:\s]+/gu),
|
|
318
|
+
];
|
|
319
|
+
if (tokens.length > 8192) return null;
|
|
320
|
+
let cursor = 0;
|
|
321
|
+
const spans = [];
|
|
322
|
+
const visit = (path, depth) => {
|
|
323
|
+
if (depth > 32) throw new Error("Bounded JSON depth");
|
|
324
|
+
const token = tokens[cursor++];
|
|
325
|
+
if (token[0] === "{") {
|
|
326
|
+
const keys = new Set();
|
|
327
|
+
while (tokens[cursor][0] !== "}") {
|
|
328
|
+
const key = JSON.parse(tokens[cursor++][0]);
|
|
329
|
+
if (keys.has(key)) throw new Error("Duplicate decoded JSON key");
|
|
330
|
+
keys.add(key);
|
|
331
|
+
cursor++; // Native JSON validation already proved the colon.
|
|
332
|
+
visit([...path, key], depth + 1);
|
|
333
|
+
if (tokens[cursor][0] === ",") cursor++;
|
|
334
|
+
}
|
|
335
|
+
cursor++;
|
|
336
|
+
} else if (token[0] === "[") {
|
|
337
|
+
let index = 0;
|
|
338
|
+
while (tokens[cursor][0] !== "]") {
|
|
339
|
+
visit([...path, index++], depth + 1);
|
|
340
|
+
if (tokens[cursor][0] === ",") cursor++;
|
|
341
|
+
}
|
|
342
|
+
cursor++;
|
|
343
|
+
} else if (
|
|
344
|
+
path.length === 2 &&
|
|
345
|
+
path[0] === "source_sha256" &&
|
|
346
|
+
token[0][0] === '"'
|
|
347
|
+
) {
|
|
348
|
+
const hash = JSON.parse(token[0]);
|
|
349
|
+
// Escaped values do not supply an unambiguous literal detector span.
|
|
350
|
+
if (token[0] !== JSON.stringify(hash))
|
|
351
|
+
throw new Error("Ambiguous digest spelling");
|
|
352
|
+
spans.push({
|
|
353
|
+
file: path[1],
|
|
354
|
+
hash,
|
|
355
|
+
start: Buffer.byteLength(text.slice(0, token.index + 1)),
|
|
356
|
+
end: Buffer.byteLength(
|
|
357
|
+
text.slice(0, token.index + token[0].length - 1)
|
|
358
|
+
),
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
visit([], 0);
|
|
363
|
+
return cursor === tokens.length && spans.length === entries.length
|
|
364
|
+
? { value, spans }
|
|
365
|
+
: null;
|
|
366
|
+
} catch {
|
|
367
|
+
// probe-direction: fail-closed — malformed, duplicate or over-budget JSON
|
|
368
|
+
// cannot establish evidence role; its detector findings remain blocking.
|
|
369
|
+
return null;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
/** Clear only a uniquely attributed, independently established benign finding. */
|
|
374
|
+
const classifyEvidence = cwd => {
|
|
375
|
+
const budget = { remaining: EVIDENCE_BUDGET };
|
|
376
|
+
const blobs = new Map();
|
|
377
|
+
const maps = new Map();
|
|
378
|
+
const blob = (file, commit) => {
|
|
379
|
+
const key = `${commit}\0${file}`;
|
|
380
|
+
if (!blobs.has(key)) {
|
|
381
|
+
if (blobs.size >= 256) return null;
|
|
382
|
+
blobs.set(key, readBlob(file, commit, cwd, budget));
|
|
383
|
+
}
|
|
384
|
+
return blobs.get(key);
|
|
385
|
+
};
|
|
386
|
+
return row => {
|
|
387
|
+
if (
|
|
388
|
+
row.RuleID !== "generic-api-key" ||
|
|
389
|
+
!safePath(row.File) ||
|
|
390
|
+
row.SymlinkFile !== "" ||
|
|
391
|
+
row.Secret !== "REDACTED" ||
|
|
392
|
+
typeof row.Match !== "string" ||
|
|
393
|
+
row.EndLine !== row.StartLine ||
|
|
394
|
+
!Number.isSafeInteger(row.StartColumn) ||
|
|
395
|
+
!Number.isSafeInteger(row.EndColumn) ||
|
|
396
|
+
row.StartColumn < 1 ||
|
|
397
|
+
row.EndColumn < row.StartColumn
|
|
398
|
+
)
|
|
399
|
+
return false;
|
|
400
|
+
const bytes = blob(row.File, row.Commit);
|
|
401
|
+
if (!bytes) return false;
|
|
402
|
+
let lineStart = 0;
|
|
403
|
+
for (let line = 1; line < row.StartLine; line++) {
|
|
404
|
+
const next = bytes.indexOf(10, lineStart);
|
|
405
|
+
if (next < 0) return false;
|
|
406
|
+
lineStart = next + 1;
|
|
407
|
+
}
|
|
408
|
+
const newline = bytes.indexOf(10, lineStart);
|
|
409
|
+
const lineEnd = newline < 0 ? bytes.length : newline;
|
|
410
|
+
const replacements = row.Match.split("REDACTED");
|
|
411
|
+
if (replacements.length !== 2) return false;
|
|
412
|
+
const line = bytes.subarray(lineStart, lineEnd);
|
|
413
|
+
const attributed = (candidate, left, right) => {
|
|
414
|
+
const template = replacements.join(candidate);
|
|
415
|
+
if (template.split(candidate).length !== 2) return false;
|
|
416
|
+
const original = Buffer.from(template);
|
|
417
|
+
const offset = line.indexOf(original);
|
|
418
|
+
if (offset < 0 || line.indexOf(original, offset + 1) >= 0) return false;
|
|
419
|
+
const start = lineStart + offset;
|
|
420
|
+
const end = start + original.length;
|
|
421
|
+
if (
|
|
422
|
+
left !== start + Buffer.byteLength(replacements[0]) ||
|
|
423
|
+
right !== left + Buffer.byteLength(candidate)
|
|
424
|
+
)
|
|
425
|
+
return false;
|
|
426
|
+
// Pinned 8.30.1 detect/location.go counts a preceding fragment newline
|
|
427
|
+
// in columns after its first line. Prove exactly one of those two byte
|
|
428
|
+
// frames against the actual unique Git span; never infer fragment state.
|
|
429
|
+
return (
|
|
430
|
+
[0, 1].filter(
|
|
431
|
+
newlineByte =>
|
|
432
|
+
start === lineStart + row.StartColumn - 1 - newlineByte &&
|
|
433
|
+
end === lineStart + row.EndColumn - newlineByte
|
|
434
|
+
).length === 1
|
|
435
|
+
);
|
|
436
|
+
};
|
|
437
|
+
// A complete standalone validation paragraph excludes surrounding JSON,
|
|
438
|
+
// assignments and interpolated/code contexts, not merely their filenames.
|
|
439
|
+
if (
|
|
440
|
+
(bytes.equals(Buffer.from(NARRATIVE)) ||
|
|
441
|
+
bytes.equals(Buffer.from(`${NARRATIVE}\n`))) &&
|
|
442
|
+
attributed(
|
|
443
|
+
TAXONOMY,
|
|
444
|
+
NARRATIVE.indexOf(TAXONOMY),
|
|
445
|
+
NARRATIVE.indexOf(TAXONOMY) + TAXONOMY.length
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
return true;
|
|
449
|
+
const key = `${row.Commit}\0${row.File}`;
|
|
450
|
+
if (!maps.has(key)) {
|
|
451
|
+
const parsed = evidenceMap(bytes);
|
|
452
|
+
let valid = parsed !== null;
|
|
453
|
+
const revision =
|
|
454
|
+
parsed && Object.hasOwn(parsed.value, "source_revision")
|
|
455
|
+
? parsed.value.source_revision
|
|
456
|
+
: row.Commit;
|
|
457
|
+
if (parsed && Object.hasOwn(parsed.value, "source_revision")) {
|
|
458
|
+
valid =
|
|
459
|
+
typeof revision === "string" &&
|
|
460
|
+
new RegExp(`^[a-f0-9]{${row.Commit.length}}$`, "u").test(revision);
|
|
461
|
+
if (valid) {
|
|
462
|
+
const type = spawnSync(
|
|
463
|
+
"git",
|
|
464
|
+
[NO_REPLACEMENTS, "cat-file", "-t", revision],
|
|
465
|
+
{
|
|
466
|
+
cwd,
|
|
467
|
+
env: gitEnvironment(),
|
|
468
|
+
timeout: 120000,
|
|
469
|
+
maxBuffer: 1024 * 1024,
|
|
470
|
+
}
|
|
471
|
+
);
|
|
472
|
+
const ancestry = spawnSync(
|
|
473
|
+
"git",
|
|
474
|
+
[
|
|
475
|
+
NO_REPLACEMENTS,
|
|
476
|
+
"merge-base",
|
|
477
|
+
"--is-ancestor",
|
|
478
|
+
revision,
|
|
479
|
+
row.Commit,
|
|
480
|
+
],
|
|
481
|
+
{
|
|
482
|
+
cwd,
|
|
483
|
+
env: gitEnvironment(),
|
|
484
|
+
timeout: 120000,
|
|
485
|
+
maxBuffer: 1024 * 1024,
|
|
486
|
+
}
|
|
487
|
+
);
|
|
488
|
+
// probe-direction: fail-closed — an absent/unreachable revision or
|
|
489
|
+
// failed ancestry probe cannot establish a historical preimage.
|
|
490
|
+
valid =
|
|
491
|
+
!type.error &&
|
|
492
|
+
!type.signal &&
|
|
493
|
+
type.status === 0 &&
|
|
494
|
+
type.stdout.toString() === "commit\n" &&
|
|
495
|
+
!type.stderr.length &&
|
|
496
|
+
!ancestry.error &&
|
|
497
|
+
!ancestry.signal &&
|
|
498
|
+
ancestry.status === 0 &&
|
|
499
|
+
!ancestry.stdout.length &&
|
|
500
|
+
!ancestry.stderr.length;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (valid)
|
|
504
|
+
for (const span of parsed.spans) {
|
|
505
|
+
const sourceBytes = blob(span.file, revision);
|
|
506
|
+
if (!sourceBytes || digest(sourceBytes) !== span.hash) {
|
|
507
|
+
valid = false;
|
|
508
|
+
break;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
maps.set(key, valid ? parsed.spans : []);
|
|
512
|
+
}
|
|
513
|
+
const candidates = maps
|
|
514
|
+
.get(key)
|
|
515
|
+
.filter(span => attributed(span.hash, span.start, span.end));
|
|
516
|
+
return candidates.length === 1;
|
|
517
|
+
};
|
|
518
|
+
};
|
|
519
|
+
|
|
225
520
|
/** Only structured safe attribution is serialized, never stderr or metadata strings. */
|
|
226
521
|
export const scanCommits = (commits, cwd, requested) => {
|
|
227
522
|
const binary = qualifyScanner(requested);
|
|
@@ -263,6 +558,7 @@ export const scanCommits = (commits, cwd, requested) => {
|
|
|
263
558
|
});
|
|
264
559
|
const findings = [];
|
|
265
560
|
const audit = auditedGit(scratch);
|
|
561
|
+
const benign = classifyEvidence(cwd);
|
|
266
562
|
for (let offset = 0; offset < commits.length; offset += 100) {
|
|
267
563
|
const group = commits.slice(offset, offset + 100);
|
|
268
564
|
audit.begin();
|
|
@@ -334,11 +630,12 @@ export const scanCommits = (commits, cwd, requested) => {
|
|
|
334
630
|
throw new HistorySecretError(
|
|
335
631
|
"Scanner attribution is invalid. Repair pinned scanner execution; no raw report is published."
|
|
336
632
|
);
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
633
|
+
if (!benign(row))
|
|
634
|
+
findings.push({
|
|
635
|
+
rule: row.RuleID,
|
|
636
|
+
commit: row.Commit,
|
|
637
|
+
line: row.StartLine,
|
|
638
|
+
});
|
|
342
639
|
}
|
|
343
640
|
}
|
|
344
641
|
return {
|
|
@@ -1077,11 +1077,36 @@ function trackerContract(config = readConfig()) {
|
|
|
1077
1077
|
);
|
|
1078
1078
|
}
|
|
1079
1079
|
|
|
1080
|
+
/**
|
|
1081
|
+
* Resolve authored references before any binding or provider decision.
|
|
1082
|
+
*
|
|
1083
|
+
* The literal `github/` namespace is an identity spelling, not a different
|
|
1084
|
+
* tracker or an exemption. Its ASCII segments must survive unchanged; URL
|
|
1085
|
+
* parsing or path normalization would incorrectly admit encoded/dot paths.
|
|
1086
|
+
* @param {unknown} raw Authored work-item reference.
|
|
1087
|
+
* @param {object} contract Existing configured tracker contract.
|
|
1088
|
+
* @returns {string} Repository-scoped canonical identity.
|
|
1089
|
+
*/
|
|
1080
1090
|
function canonicalizeRef(raw, contract = trackerContract()) {
|
|
1081
1091
|
const value = String(raw ?? "").trim();
|
|
1082
1092
|
if (contract.provider === "github") {
|
|
1083
|
-
const
|
|
1084
|
-
|
|
1093
|
+
const qualified =
|
|
1094
|
+
/^github\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)#([1-9]\d*)$/.exec(value);
|
|
1095
|
+
// Match authored URL bytes before normalization: URL parsers can erase a
|
|
1096
|
+
// port or decode a path that this input contract deliberately refuses.
|
|
1097
|
+
const match =
|
|
1098
|
+
qualified ??
|
|
1099
|
+
/^([^\s/#]+\/[^\s/#]+)#([1-9]\d*)$/.exec(value) ??
|
|
1100
|
+
/^https:\/\/github\.com\/([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+)\/issues\/([1-9]\d*)$/.exec(
|
|
1101
|
+
value
|
|
1102
|
+
);
|
|
1103
|
+
if (
|
|
1104
|
+
!match ||
|
|
1105
|
+
(qualified &&
|
|
1106
|
+
qualified[1]
|
|
1107
|
+
.split("/")
|
|
1108
|
+
.some(segment => segment === "." || segment === ".."))
|
|
1109
|
+
)
|
|
1085
1110
|
throw new TrackingError(
|
|
1086
1111
|
`Invalid GitHub Work-Item '${value}'; expected owner/repo#123`
|
|
1087
1112
|
);
|
|
@@ -1438,6 +1463,18 @@ export function soleWorkItem(text, contract, subject) {
|
|
|
1438
1463
|
const refs = [
|
|
1439
1464
|
...new Set(values.map(value => canonicalizeRef(value, contract))),
|
|
1440
1465
|
];
|
|
1466
|
+
// Canonical repeats (including repository case variants) retain their
|
|
1467
|
+
// meaning. Mixing the URL and canonical representations is two authored
|
|
1468
|
+
// declarations even when normalization would collapse their identity.
|
|
1469
|
+
if (
|
|
1470
|
+
contract.provider === "github" &&
|
|
1471
|
+
values.some(value => value.startsWith("https://github.com/")) &&
|
|
1472
|
+
values.some(value => !value.startsWith("https://github.com/"))
|
|
1473
|
+
) {
|
|
1474
|
+
throw new TrackingError(
|
|
1475
|
+
`${subject[0].toUpperCase()}${subject.slice(1)} mixes URL and canonical Work-Item declarations; it must name exactly one`
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1441
1478
|
if (refs.length > 1) {
|
|
1442
1479
|
throw new TrackingError(
|
|
1443
1480
|
`${subject[0].toUpperCase()}${subject.slice(1)} names ${refs.length} different work items (${refs.join(", ")}); it must name exactly one`
|
|
@@ -3526,7 +3563,24 @@ function prWorkItems(body, contract) {
|
|
|
3526
3563
|
throw new TrackingError(
|
|
3527
3564
|
"No Work-Item trailer anywhere in the pull request body"
|
|
3528
3565
|
);
|
|
3529
|
-
|
|
3566
|
+
const declared = values.map(value => ({
|
|
3567
|
+
value,
|
|
3568
|
+
ref: canonicalizeRef(value, contract),
|
|
3569
|
+
}));
|
|
3570
|
+
// Validation precedes representation checks; canonical GitHub refs lack ://.
|
|
3571
|
+
const urlRefs = new Set(
|
|
3572
|
+
declared.filter(({ value }) => value.includes("://")).map(({ ref }) => ref)
|
|
3573
|
+
);
|
|
3574
|
+
if (
|
|
3575
|
+
contract.provider === "github" &&
|
|
3576
|
+
declared.some(
|
|
3577
|
+
({ value, ref }) => !value.includes("://") && urlRefs.has(ref)
|
|
3578
|
+
)
|
|
3579
|
+
)
|
|
3580
|
+
throw new TrackingError(
|
|
3581
|
+
"Pull request body mixes URL and canonical Work-Item declarations for the same work item"
|
|
3582
|
+
);
|
|
3583
|
+
return [...new Set(declared.map(({ ref }) => ref))];
|
|
3530
3584
|
}
|
|
3531
3585
|
|
|
3532
3586
|
/**
|
|
@@ -4507,6 +4561,10 @@ function backlink(args) {
|
|
|
4507
4561
|
* repository does the commit declare? — and a value the filter drops is one the
|
|
4508
4562
|
* reader still saw. That ordering is what keeps a malformed trailer refusable
|
|
4509
4563
|
* by the gate instead of invisible to it.
|
|
4564
|
+
* Qualified `github/owner/name#number` values use the SAME canonical resolver
|
|
4565
|
+
* as the gates, so the audit does not lose attribution accepted by a hook.
|
|
4566
|
+
* This audit still requires a hash-number declaration; supported issue URLs
|
|
4567
|
+
* remain a gate input, not a newly admitted audit declaration.
|
|
4510
4568
|
*
|
|
4511
4569
|
* CRLF needs nothing here, and a relayed review finding that said otherwise
|
|
4512
4570
|
* was refuted rather than acted on. `workItemLines` splits on `/\r?\n/`, so a
|
|
@@ -4518,16 +4576,19 @@ function backlink(args) {
|
|
|
4518
4576
|
* @returns {number[]} Declared issue numbers, de-duplicated, in first-seen order.
|
|
4519
4577
|
*/
|
|
4520
4578
|
export function declaredWorkItemNumbers(body, repository) {
|
|
4521
|
-
const owner = String(repository ?? "")
|
|
4579
|
+
const owner = String(repository ?? "");
|
|
4522
4580
|
if (owner === "") return [];
|
|
4581
|
+
const contract = { provider: "github", repository: owner };
|
|
4523
4582
|
const numbers = [];
|
|
4524
4583
|
for (const value of workItemLines(body)) {
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4584
|
+
if (!value.includes("#")) continue;
|
|
4585
|
+
try {
|
|
4586
|
+
const ref = canonicalizeRef(value, contract);
|
|
4587
|
+
const digits = ref.slice(ref.indexOf("#") + 1);
|
|
4588
|
+
if (isIssueNumber(digits)) numbers.push(Number(digits));
|
|
4589
|
+
} catch (error) {
|
|
4590
|
+
if (!(error instanceof TrackingError)) throw error;
|
|
4591
|
+
}
|
|
4531
4592
|
}
|
|
4532
4593
|
return [...new Set(numbers)];
|
|
4533
4594
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"lisa-owned-hash-ledger.d.ts","sourceRoot":"","sources":["../../src/core/lisa-owned-hash-ledger.ts"],"names":[],"mappings":"AACA,6EAA6E;AAE7E;;;;;;;GAOG;AACH,eAAO,MAAM,sBAAsB,EAAE,QAAQ,CAC3C,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAy7CjC,CAAC;AAEH;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,+BAA+B,EAAE,QAAQ,CACpD,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CA41CjC,CAAC"}
|
|
@@ -456,8 +456,11 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
456
456
|
"ca75ebbc45fca4c52400612ae818d231a28289b50a70f6cdee6feb51b144896f",
|
|
457
457
|
]),
|
|
458
458
|
"scripts/lib/history-secret-scanner.mjs": Object.freeze([
|
|
459
|
+
"223cc0e59b19b60e0e78a8a85710cab634ef90cd4661b5a5d608455afbf9c4c4",
|
|
459
460
|
"501452c19be923616a6ea1ed9443c86cbac14e4aa3073e579f6af82d1423a2c9",
|
|
460
461
|
"6afcb5ea42d8f9046a17e2253d4ddbefdf9025b150870afda23db77796f05f25",
|
|
462
|
+
"a68a4ba784699c470e1525ed3cd2f31f399cc80472d2bf5653d936bf3252c658",
|
|
463
|
+
"fc2677b9f5ae86ee9709bb59f1e9cf231f258d12ddcfbf9a6dd9a711155220d2",
|
|
461
464
|
"ff90535b7c6f24f48651ab93d204a1530c6bfaa8e6169d3aa83ab069c1a1ef22",
|
|
462
465
|
]),
|
|
463
466
|
"scripts/lib/invoked-as-script.mjs": Object.freeze([
|
|
@@ -1224,6 +1227,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
1224
1227
|
"04f78df45465d01601c8b5711b74749b2a2c2b70b9c916d6790bd69e1c11865d",
|
|
1225
1228
|
"06f82a176755c7ff464e263c8a0a6249e02bed710504acf1129ff8fc4c2c0326",
|
|
1226
1229
|
"08ec9a37d66fb35443a5aac72e09d7ab5478cbd516f72e248d01b3befabddfe5",
|
|
1230
|
+
"0a9d388ac78181e23aa4a23d6b2e9af04b30f204f39a0378a5bbd60612d907ce",
|
|
1227
1231
|
"0b6eff0a306ccd8396d6e3d71ac9c54d3a1c8c29449a9146e0c2e5726b74feb9",
|
|
1228
1232
|
"0fdec8bbfb55e97e9caa91441c8306b0f7d0b9a8957b80ce68fbe98346e0d63e",
|
|
1229
1233
|
"10b9e0e2db549609fb4ab66ebe9b4f755bf7f6cc6be606f1a3515f5ebe6d5110",
|
|
@@ -1239,6 +1243,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
1239
1243
|
"23b58ea56cb26fbdef204d7bdbc4f18fe097947d64f733381823e900a30ea70c",
|
|
1240
1244
|
"250d44a7a862e59cbbc35f66e8dd28e68c9a9df7cd1c27c547b3aadadc2b5956",
|
|
1241
1245
|
"275b4d773ecefeeceee972191086cffaa159a4cb3cac2dae3d85dfd363c69fb6",
|
|
1246
|
+
"2a7528b1362a5e73c01874d5dd915f030f023c12621bfe87830295480baaa8e8",
|
|
1242
1247
|
"2b7e81f0e23628da9c3a124f3d5b0593e3a8c564cd595329b9f37510540a5581",
|
|
1243
1248
|
"2c678c75d6de9b029f27fa6eec45b8421217a4fb5ef3412ad39883e10b213987",
|
|
1244
1249
|
"2cd876ccf562b8c2b146817f80ebc189f9e31989457067c14938ed5e745a94b9",
|
|
@@ -1255,6 +1260,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
1255
1260
|
"42176fa5ee6df11d2337a91b2eeeb31a9ed7cc1e16acb0cec11739ba5a6849ed",
|
|
1256
1261
|
"4372872a448d3b943dea5541167ed5be6155703b57b9f8224d19ddefd3b80898",
|
|
1257
1262
|
"475cb9ef86c2ac54c8fd8cb6d63e9a55ea8cd0017e26efbe3c44391a4aeeab41",
|
|
1263
|
+
"47677cf8b388e62fc82efdf564f94a784499f23b7f290ee1d7f9f50cc8a8abc3",
|
|
1258
1264
|
"4ae4e4e25c5fc075824a112a26abae6ab157c2888332c376db928b51530abc0a",
|
|
1259
1265
|
"4c263fef48c03b3b6c623ea8a6cd55505bfaf28197e5072226ea35f9f3d26885",
|
|
1260
1266
|
"4d054e3b7a266d69108f7cec12ee02e6a302670ff5932dc398cb0c5069b2a44f",
|
|
@@ -1282,6 +1288,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
1282
1288
|
"6afe796f5a69a68a026c79a64aa451eb2bf7907a0ace0d9079f8b4add16b886b",
|
|
1283
1289
|
"6ced6d3929f58e0b0b3e12717af537f9c4f57ad03c18d99e774026c169b13874",
|
|
1284
1290
|
"6dadc065016e741568b6ae1ebcd0378085005c076fdb93346bad7c1fd9e3b8b8",
|
|
1291
|
+
"6ec811517f212f6e6b33a8163f16a6b0538731c9cbe626e9f62a55b0f0af9518",
|
|
1285
1292
|
"7156449f0ad8c68c40c3f7f02df91248e32d9d83a439b38c4ce0a3fd35e04c1e",
|
|
1286
1293
|
"71fcbf510d3ac112417cea1bd0b4fd4b478e7f01a51f04b6a28fb7a487443d36",
|
|
1287
1294
|
"71ff42c2cb8ac7022e0066132476764dd745fb92bf19ccd395000b1fe319abc7",
|
|
@@ -1345,6 +1352,7 @@ export const LISA_OWNED_HASH_LEDGER = Object.freeze({
|
|
|
1345
1352
|
"e41e9534988ce69a836664437a78f3a3f3dc7ebbe396c86cdab0ea63fb7c19da",
|
|
1346
1353
|
"e5e7d4322195d0e675fd62a8630cefa811d2459cea752163787d01cfa65e5ed7",
|
|
1347
1354
|
"e80c460034868c7e4152dd4276881105f80b3b76f4f130bff1ab662e17a9cb5a",
|
|
1355
|
+
"e94675ad68dd4254ad96fb1f2f67eb58447d4eb4f04475200d449f4932c78824",
|
|
1348
1356
|
"ea63f5b9e02ed081e21bebd928e77b3f7315addd2b8001112cb423eea459cd5f",
|
|
1349
1357
|
"ea82b8b47c8367fc732ed8c70ba2cbf882aa01b40e40d40a95c5874044206815",
|
|
1350
1358
|
"ec2aa4c645fb1573aa2dfb731e7ced02a00b7f56a3eb853157ac7e74c0a04f92",
|
|
@@ -1907,8 +1915,11 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
1907
1915
|
"ca75ebbc45fca4c52400612ae818d231a28289b50a70f6cdee6feb51b144896f",
|
|
1908
1916
|
]),
|
|
1909
1917
|
"scripts/lib/history-secret-scanner.mjs": Object.freeze([
|
|
1918
|
+
"223cc0e59b19b60e0e78a8a85710cab634ef90cd4661b5a5d608455afbf9c4c4",
|
|
1910
1919
|
"501452c19be923616a6ea1ed9443c86cbac14e4aa3073e579f6af82d1423a2c9",
|
|
1911
1920
|
"6afcb5ea42d8f9046a17e2253d4ddbefdf9025b150870afda23db77796f05f25",
|
|
1921
|
+
"a68a4ba784699c470e1525ed3cd2f31f399cc80472d2bf5653d936bf3252c658",
|
|
1922
|
+
"fc2677b9f5ae86ee9709bb59f1e9cf231f258d12ddcfbf9a6dd9a711155220d2",
|
|
1912
1923
|
"ff90535b7c6f24f48651ab93d204a1530c6bfaa8e6169d3aa83ab069c1a1ef22",
|
|
1913
1924
|
]),
|
|
1914
1925
|
"scripts/lib/invoked-as-script.mjs": Object.freeze([
|
|
@@ -2607,6 +2618,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
2607
2618
|
"04401ae05bf9464e6929180ffdfff80afc528a1df20733b032a0342252ca1d4a",
|
|
2608
2619
|
"06f82a176755c7ff464e263c8a0a6249e02bed710504acf1129ff8fc4c2c0326",
|
|
2609
2620
|
"08ec9a37d66fb35443a5aac72e09d7ab5478cbd516f72e248d01b3befabddfe5",
|
|
2621
|
+
"0a9d388ac78181e23aa4a23d6b2e9af04b30f204f39a0378a5bbd60612d907ce",
|
|
2610
2622
|
"0b6eff0a306ccd8396d6e3d71ac9c54d3a1c8c29449a9146e0c2e5726b74feb9",
|
|
2611
2623
|
"0fdec8bbfb55e97e9caa91441c8306b0f7d0b9a8957b80ce68fbe98346e0d63e",
|
|
2612
2624
|
"10b9e0e2db549609fb4ab66ebe9b4f755bf7f6cc6be606f1a3515f5ebe6d5110",
|
|
@@ -2622,6 +2634,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
2622
2634
|
"23b58ea56cb26fbdef204d7bdbc4f18fe097947d64f733381823e900a30ea70c",
|
|
2623
2635
|
"250d44a7a862e59cbbc35f66e8dd28e68c9a9df7cd1c27c547b3aadadc2b5956",
|
|
2624
2636
|
"275b4d773ecefeeceee972191086cffaa159a4cb3cac2dae3d85dfd363c69fb6",
|
|
2637
|
+
"2a7528b1362a5e73c01874d5dd915f030f023c12621bfe87830295480baaa8e8",
|
|
2625
2638
|
"2b7e81f0e23628da9c3a124f3d5b0593e3a8c564cd595329b9f37510540a5581",
|
|
2626
2639
|
"2c678c75d6de9b029f27fa6eec45b8421217a4fb5ef3412ad39883e10b213987",
|
|
2627
2640
|
"2cd876ccf562b8c2b146817f80ebc189f9e31989457067c14938ed5e745a94b9",
|
|
@@ -2638,6 +2651,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
2638
2651
|
"42176fa5ee6df11d2337a91b2eeeb31a9ed7cc1e16acb0cec11739ba5a6849ed",
|
|
2639
2652
|
"4372872a448d3b943dea5541167ed5be6155703b57b9f8224d19ddefd3b80898",
|
|
2640
2653
|
"475cb9ef86c2ac54c8fd8cb6d63e9a55ea8cd0017e26efbe3c44391a4aeeab41",
|
|
2654
|
+
"47677cf8b388e62fc82efdf564f94a784499f23b7f290ee1d7f9f50cc8a8abc3",
|
|
2641
2655
|
"4ae4e4e25c5fc075824a112a26abae6ab157c2888332c376db928b51530abc0a",
|
|
2642
2656
|
"4c263fef48c03b3b6c623ea8a6cd55505bfaf28197e5072226ea35f9f3d26885",
|
|
2643
2657
|
"4d054e3b7a266d69108f7cec12ee02e6a302670ff5932dc398cb0c5069b2a44f",
|
|
@@ -2664,6 +2678,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
2664
2678
|
"6afe796f5a69a68a026c79a64aa451eb2bf7907a0ace0d9079f8b4add16b886b",
|
|
2665
2679
|
"6ced6d3929f58e0b0b3e12717af537f9c4f57ad03c18d99e774026c169b13874",
|
|
2666
2680
|
"6dadc065016e741568b6ae1ebcd0378085005c076fdb93346bad7c1fd9e3b8b8",
|
|
2681
|
+
"6ec811517f212f6e6b33a8163f16a6b0538731c9cbe626e9f62a55b0f0af9518",
|
|
2667
2682
|
"7156449f0ad8c68c40c3f7f02df91248e32d9d83a439b38c4ce0a3fd35e04c1e",
|
|
2668
2683
|
"71fcbf510d3ac112417cea1bd0b4fd4b478e7f01a51f04b6a28fb7a487443d36",
|
|
2669
2684
|
"71ff42c2cb8ac7022e0066132476764dd745fb92bf19ccd395000b1fe319abc7",
|
|
@@ -2726,6 +2741,7 @@ export const LISA_OWNED_HASH_HISTORY_DERIVED = Object.freeze({
|
|
|
2726
2741
|
"e41e9534988ce69a836664437a78f3a3f3dc7ebbe396c86cdab0ea63fb7c19da",
|
|
2727
2742
|
"e5e7d4322195d0e675fd62a8630cefa811d2459cea752163787d01cfa65e5ed7",
|
|
2728
2743
|
"e80c460034868c7e4152dd4276881105f80b3b76f4f130bff1ab662e17a9cb5a",
|
|
2744
|
+
"e94675ad68dd4254ad96fb1f2f67eb58447d4eb4f04475200d449f4932c78824",
|
|
2729
2745
|
"ea63f5b9e02ed081e21bebd928e77b3f7315addd2b8001112cb423eea459cd5f",
|
|
2730
2746
|
"ea82b8b47c8367fc732ed8c70ba2cbf882aa01b40e40d40a95c5874044206815",
|
|
2731
2747
|
"ec2aa4c645fb1573aa2dfb731e7ced02a00b7f56a3eb853157ac7e74c0a04f92",
|