@meetless/mla 0.1.4 → 0.1.6
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/dist/build-info.json +3 -3
- package/dist/cli.js +31 -5
- package/dist/commands/activate.js +39 -18
- package/dist/commands/agent-memory.js +333 -0
- package/dist/commands/enrich.js +211 -2
- package/dist/commands/internal-auto-index.js +64 -1
- package/dist/commands/internal-pretool-observe.js +86 -1
- package/dist/commands/internal-redact-capture.js +130 -0
- package/dist/commands/pilot.js +385 -0
- package/dist/lib/agent-memory-capture/binding.js +115 -0
- package/dist/lib/agent-memory-capture/classify.js +68 -0
- package/dist/lib/agent-memory-capture/collector.js +69 -0
- package/dist/lib/agent-memory-capture/containment.js +74 -0
- package/dist/lib/agent-memory-capture/ledger.js +43 -0
- package/dist/lib/agent-memory-capture/live-collector.js +148 -0
- package/dist/lib/agent-memory-capture/live-ledger.js +45 -0
- package/dist/lib/agent-memory-capture/live-pipeline.js +344 -0
- package/dist/lib/agent-memory-capture/lock.js +98 -0
- package/dist/lib/agent-memory-capture/paths.js +47 -0
- package/dist/lib/agent-memory-capture/pipeline.js +222 -0
- package/dist/lib/agent-memory-capture/report.js +131 -0
- package/dist/lib/agent-memory-capture/types.js +14 -0
- package/dist/lib/agent-memory-capture/upsert-client.js +104 -0
- package/dist/lib/analytics/enforcement-classify.js +65 -0
- package/dist/lib/analytics/enforcement-incident.js +83 -0
- package/dist/lib/analytics/envelope.js +55 -1
- package/dist/lib/analytics/pilot.js +313 -0
- package/dist/lib/enrichment/ingest.js +98 -13
- package/dist/lib/enrichment/materialize-rules.js +81 -0
- package/dist/lib/enrichment/plan.js +72 -15
- package/dist/lib/enrichment/protocol.js +85 -5
- package/dist/lib/enrichment/scout-brief.js +35 -6
- package/dist/lib/redactor.js +104 -1
- package/dist/lib/scanner/agent-memory.js +55 -4
- package/dist/lib/scanner/managed-rules.js +0 -0
- package/dist/lib/scanner/scan.js +52 -1
- package/dist/lib/scanner/score.js +41 -3
- package/dist/lib/scanner/scout-mission.js +9 -7
- package/dist/lib/upgrade-apply.js +30 -0
- package/dist/lib/wire.js +2 -0
- package/package.json +3 -3
package/dist/commands/enrich.js
CHANGED
|
@@ -19,10 +19,16 @@ exports.resolveBudgetMs = resolveBudgetMs;
|
|
|
19
19
|
exports.parsePlanArgs = parsePlanArgs;
|
|
20
20
|
exports.parseIngestArgs = parseIngestArgs;
|
|
21
21
|
exports.extractResults = extractResults;
|
|
22
|
+
exports.renderIngestSummary = renderIngestSummary;
|
|
22
23
|
exports.parseBriefArgs = parseBriefArgs;
|
|
24
|
+
exports.parseMaterializeArgs = parseMaterializeArgs;
|
|
25
|
+
exports.extractAcceptedCandidates = extractAcceptedCandidates;
|
|
26
|
+
exports.validateAcceptedCandidates = validateAcceptedCandidates;
|
|
27
|
+
exports.renderMaterializeSummary = renderMaterializeSummary;
|
|
23
28
|
exports.runEnrich = runEnrich;
|
|
24
29
|
const node_child_process_1 = require("node:child_process");
|
|
25
30
|
const node_fs_1 = require("node:fs");
|
|
31
|
+
const node_path_1 = require("node:path");
|
|
26
32
|
const node_crypto_1 = require("node:crypto");
|
|
27
33
|
const config_1 = require("../lib/config");
|
|
28
34
|
const workspace_1 = require("../lib/workspace");
|
|
@@ -31,7 +37,9 @@ const plan_1 = require("../lib/enrichment/plan");
|
|
|
31
37
|
const ingest_1 = require("../lib/enrichment/ingest");
|
|
32
38
|
const scout_brief_1 = require("../lib/enrichment/scout-brief");
|
|
33
39
|
const protocol_1 = require("../lib/enrichment/protocol");
|
|
34
|
-
const
|
|
40
|
+
const managed_rules_1 = require("../lib/scanner/managed-rules");
|
|
41
|
+
const materialize_rules_1 = require("../lib/enrichment/materialize-rules");
|
|
42
|
+
const USAGE = `mla enrich: agent-orchestrated onboarding enrichment.
|
|
35
43
|
|
|
36
44
|
mla enrich plan [--json] [--budget-ms <n>] [--workspace <id>]
|
|
37
45
|
Scan this repository into an immutable run record and print the plan the
|
|
@@ -50,7 +58,16 @@ const USAGE = `mla enrich: agent-orchestrated onboarding enrichment (two bookend
|
|
|
50
58
|
stdin when no file is given (an array of scout results, or an object with a
|
|
51
59
|
\`results\` array). Candidates land in the governed KB born PENDING.
|
|
52
60
|
Exit: 0 clean, 1 a scout needs attention (persistence failed / malformed),
|
|
53
|
-
2 the request was rejected (unknown run, mismatch, corrupt record)
|
|
61
|
+
2 the request was rejected (unknown run, mismatch, corrupt record).
|
|
62
|
+
|
|
63
|
+
mla enrich materialize [--accepted-file <path>] [--dry-run] [--json]
|
|
64
|
+
Write the accepted DURABLE rules (constraint, convention, boundary) into the
|
|
65
|
+
mla-managed rule file (.meetless/rules.md), the one canonical source the local
|
|
66
|
+
scanner injects from. Reads the accepted candidates as JSON from --accepted-file,
|
|
67
|
+
or from stdin (a bare array, or an object with an \`accepted\` array). Decisions and
|
|
68
|
+
deprecations are reported as skipped, never written. Local only: no commit, no push
|
|
69
|
+
(prints "Effective locally. Commit and push to share with teammates."). --dry-run
|
|
70
|
+
reports the change without writing. Exit: 0 done (or nothing to do), 2 bad input.`;
|
|
54
71
|
// Mirror kb_add's ingest timeout heuristic (it is module-private there). Generous,
|
|
55
72
|
// scales with document count: the kb-add route runs the full atomic-claim pipeline.
|
|
56
73
|
function ingestTimeoutMs(docCount) {
|
|
@@ -242,13 +259,31 @@ function extractResults(raw, runId) {
|
|
|
242
259
|
}
|
|
243
260
|
function renderIngestSummary(outcomes, status) {
|
|
244
261
|
const lines = [`Onboarding ingest complete (state: ${status ?? "unknown"}).`, ``];
|
|
262
|
+
let totalPersisted = 0;
|
|
245
263
|
for (const o of outcomes) {
|
|
264
|
+
totalPersisted += o.persisted;
|
|
246
265
|
lines.push(` ${o.scout}: ${o.accepted} accepted, ${o.rejected} rejected, ${o.persisted} persisted (received ${o.received})`);
|
|
247
266
|
for (const e of o.errors) {
|
|
248
267
|
const where = e.index >= 0 ? `candidate ${e.index}` : "scout";
|
|
249
268
|
lines.push(` - ${where}: ${e.code} (${e.message})`);
|
|
250
269
|
}
|
|
251
270
|
}
|
|
271
|
+
// Review handoff: candidates land born PENDING, so the human reviews them next. The
|
|
272
|
+
// extraction cap (up to 20 per run) is NOT the review batch: show the first
|
|
273
|
+
// REVIEW_BATCH_DEFAULT and keep the rest behind "show more" so a big run does not bury
|
|
274
|
+
// the reviewer (Phase 2). When everything fits in one batch, say so plainly.
|
|
275
|
+
if (totalPersisted > 0) {
|
|
276
|
+
const batch = (0, protocol_1.selectReviewBatch)(totalPersisted);
|
|
277
|
+
lines.push(``);
|
|
278
|
+
if (batch.hasMore) {
|
|
279
|
+
lines.push(`Next: review ${totalPersisted} candidate${totalPersisted === 1 ? "" : "s"} born PENDING. ` +
|
|
280
|
+
`Run \`mla review\` to see the first ${batch.shown}; ${batch.remaining} more are behind "show more".`);
|
|
281
|
+
}
|
|
282
|
+
else {
|
|
283
|
+
lines.push(`Next: review ${totalPersisted} candidate${totalPersisted === 1 ? "" : "s"} born PENDING with \`mla review\`. ` +
|
|
284
|
+
`Nothing is accepted until you say so.`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
252
287
|
return lines.join("\n");
|
|
253
288
|
}
|
|
254
289
|
async function runEnrichIngest(argv) {
|
|
@@ -405,6 +440,178 @@ function runEnrichBrief(argv) {
|
|
|
405
440
|
console.log((0, scout_brief_1.buildScoutPrompt)(run, flags.role));
|
|
406
441
|
return 0;
|
|
407
442
|
}
|
|
443
|
+
function parseMaterializeArgs(argv) {
|
|
444
|
+
const flags = { json: false, dryRun: false };
|
|
445
|
+
for (let i = 0; i < argv.length; i++) {
|
|
446
|
+
const a = argv[i];
|
|
447
|
+
if (a === "--json")
|
|
448
|
+
flags.json = true;
|
|
449
|
+
else if (a === "--dry-run")
|
|
450
|
+
flags.dryRun = true;
|
|
451
|
+
else if (a === "--accepted-file") {
|
|
452
|
+
flags.acceptedFile = argv[++i];
|
|
453
|
+
if (!flags.acceptedFile)
|
|
454
|
+
throw new Error("--accepted-file requires a path");
|
|
455
|
+
}
|
|
456
|
+
else
|
|
457
|
+
throw new Error(`Unknown flag for \`mla enrich materialize\`: ${a}`);
|
|
458
|
+
}
|
|
459
|
+
return flags;
|
|
460
|
+
}
|
|
461
|
+
// Normalize the accepted payload into the candidate array. Accept a bare array, or an
|
|
462
|
+
// object with an `accepted` array (the natural name for "the ones the human accepted"),
|
|
463
|
+
// or a `candidates` array (so an onboard scout-result candidate list pastes through).
|
|
464
|
+
function extractAcceptedCandidates(raw) {
|
|
465
|
+
let parsed;
|
|
466
|
+
try {
|
|
467
|
+
parsed = JSON.parse(raw);
|
|
468
|
+
}
|
|
469
|
+
catch (e) {
|
|
470
|
+
throw new Error(`accepted candidates are not valid JSON: ${e.message}`);
|
|
471
|
+
}
|
|
472
|
+
if (Array.isArray(parsed))
|
|
473
|
+
return parsed;
|
|
474
|
+
if (parsed && typeof parsed === "object") {
|
|
475
|
+
const obj = parsed;
|
|
476
|
+
if (Array.isArray(obj.accepted))
|
|
477
|
+
return obj.accepted;
|
|
478
|
+
if (Array.isArray(obj.candidates))
|
|
479
|
+
return obj.candidates;
|
|
480
|
+
}
|
|
481
|
+
throw new Error("accepted candidates must be a JSON array, or an object with an `accepted` (or `candidates`) array");
|
|
482
|
+
}
|
|
483
|
+
function validateAcceptedCandidates(raw) {
|
|
484
|
+
const candidates = [];
|
|
485
|
+
const errors = [];
|
|
486
|
+
raw.forEach((r, i) => {
|
|
487
|
+
const res = (0, protocol_1.validateCandidateShape)(r, i);
|
|
488
|
+
if (res.ok)
|
|
489
|
+
candidates.push(res.candidate);
|
|
490
|
+
else
|
|
491
|
+
errors.push(...res.errors);
|
|
492
|
+
});
|
|
493
|
+
if (errors.length > 0)
|
|
494
|
+
return { ok: false, errors };
|
|
495
|
+
return { ok: true, candidates };
|
|
496
|
+
}
|
|
497
|
+
// Human-readable summary. Pure (no fs) and exported so its wording is pinned by a test.
|
|
498
|
+
// `dryRun` flips the verb from "materialized" to "would materialize" and suppresses the
|
|
499
|
+
// share line (nothing was written, so there is nothing local to share yet).
|
|
500
|
+
function renderMaterializeSummary(result, relPath, dryRun) {
|
|
501
|
+
const lines = [];
|
|
502
|
+
const added = [...result.materialized].sort((a, b) => a.statement.localeCompare(b.statement));
|
|
503
|
+
const skipped = [...result.skipped].sort((a, b) => a.statement.localeCompare(b.statement));
|
|
504
|
+
if (added.length > 0) {
|
|
505
|
+
const verb = dryRun ? "Would materialize" : "Materialized";
|
|
506
|
+
lines.push(`${verb} ${added.length} durable rule${added.length === 1 ? "" : "s"} into ${relPath}:`);
|
|
507
|
+
for (const r of added)
|
|
508
|
+
lines.push(` + ${r.statement}`);
|
|
509
|
+
}
|
|
510
|
+
else {
|
|
511
|
+
lines.push(`No durable rules to materialize (${relPath} unchanged).`);
|
|
512
|
+
}
|
|
513
|
+
if (skipped.length > 0) {
|
|
514
|
+
lines.push("");
|
|
515
|
+
lines.push(`Skipped ${skipped.length} non-rule candidate${skipped.length === 1 ? "" : "s"}:`);
|
|
516
|
+
for (const s of skipped) {
|
|
517
|
+
const why = s.reason === "empty_statement" ? "empty statement" : `${s.kind} (governed knowledge, not a rule)`;
|
|
518
|
+
lines.push(` - ${s.statement || "(empty)"}: ${why}`);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
if (result.changed && !dryRun) {
|
|
522
|
+
lines.push("");
|
|
523
|
+
lines.push(materialize_rules_1.MATERIALIZE_SHARE_MESSAGE);
|
|
524
|
+
}
|
|
525
|
+
return lines.join("\n");
|
|
526
|
+
}
|
|
527
|
+
function readManagedFile(path) {
|
|
528
|
+
try {
|
|
529
|
+
return (0, node_fs_1.readFileSync)(path, "utf8");
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return ""; // missing file is the empty starting point, not an error
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
// Atomic write: render to a sibling temp file then rename, so a crash mid-write never
|
|
536
|
+
// leaves a half-written rules file (the scanner reads it directly from disk every turn).
|
|
537
|
+
function writeManagedFile(path, text) {
|
|
538
|
+
(0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(path), { recursive: true });
|
|
539
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
540
|
+
(0, node_fs_1.writeFileSync)(tmp, text, "utf8");
|
|
541
|
+
(0, node_fs_1.renameSync)(tmp, path);
|
|
542
|
+
}
|
|
543
|
+
async function runEnrichMaterialize(argv) {
|
|
544
|
+
let flags;
|
|
545
|
+
try {
|
|
546
|
+
flags = parseMaterializeArgs(argv);
|
|
547
|
+
}
|
|
548
|
+
catch (e) {
|
|
549
|
+
console.error(e.message);
|
|
550
|
+
return 2;
|
|
551
|
+
}
|
|
552
|
+
// Local-only: the managed file lives at the git root. No workspace id, no auth.
|
|
553
|
+
let repositoryRoot;
|
|
554
|
+
try {
|
|
555
|
+
repositoryRoot = resolveRepositoryRoot(process.cwd());
|
|
556
|
+
}
|
|
557
|
+
catch (e) {
|
|
558
|
+
console.error(e.message);
|
|
559
|
+
return 2;
|
|
560
|
+
}
|
|
561
|
+
let raw;
|
|
562
|
+
try {
|
|
563
|
+
if (flags.acceptedFile) {
|
|
564
|
+
raw = (0, node_fs_1.readFileSync)(flags.acceptedFile, "utf8");
|
|
565
|
+
}
|
|
566
|
+
else if (!process.stdin.isTTY) {
|
|
567
|
+
raw = await readStdin();
|
|
568
|
+
}
|
|
569
|
+
else {
|
|
570
|
+
console.error("provide --accepted-file <path> or pipe the accepted candidates JSON to stdin");
|
|
571
|
+
return 2;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
catch (e) {
|
|
575
|
+
console.error(`could not read accepted candidates: ${e.message}`);
|
|
576
|
+
return 2;
|
|
577
|
+
}
|
|
578
|
+
let rawCandidates;
|
|
579
|
+
try {
|
|
580
|
+
rawCandidates = extractAcceptedCandidates(raw);
|
|
581
|
+
}
|
|
582
|
+
catch (e) {
|
|
583
|
+
console.error(e.message);
|
|
584
|
+
return 2;
|
|
585
|
+
}
|
|
586
|
+
const validation = validateAcceptedCandidates(rawCandidates);
|
|
587
|
+
if (!validation.ok) {
|
|
588
|
+
console.error(`refusing to materialize: ${validation.errors.length} invalid candidate(s).`);
|
|
589
|
+
for (const e of validation.errors) {
|
|
590
|
+
console.error(` - candidate ${e.index}: ${e.code}${e.field ? ` (${e.field})` : ""} ${e.message}`);
|
|
591
|
+
}
|
|
592
|
+
return 2;
|
|
593
|
+
}
|
|
594
|
+
const managedPath = (0, node_path_1.join)(repositoryRoot, managed_rules_1.MANAGED_RULES_PATH);
|
|
595
|
+
const existing = readManagedFile(managedPath);
|
|
596
|
+
const result = (0, materialize_rules_1.materializeRules)(existing, validation.candidates);
|
|
597
|
+
if (result.changed && !flags.dryRun) {
|
|
598
|
+
writeManagedFile(managedPath, result.text);
|
|
599
|
+
}
|
|
600
|
+
if (flags.json) {
|
|
601
|
+
console.log(JSON.stringify({
|
|
602
|
+
path: managed_rules_1.MANAGED_RULES_PATH,
|
|
603
|
+
changed: result.changed,
|
|
604
|
+
wrote: result.changed && !flags.dryRun,
|
|
605
|
+
dryRun: flags.dryRun,
|
|
606
|
+
materialized: result.materialized.map((r) => ({ id: r.id, statement: r.statement, strength: r.strength })),
|
|
607
|
+
skipped: result.skipped,
|
|
608
|
+
}, null, 2));
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
console.log(renderMaterializeSummary(result, managed_rules_1.MANAGED_RULES_PATH, flags.dryRun));
|
|
612
|
+
}
|
|
613
|
+
return 0;
|
|
614
|
+
}
|
|
408
615
|
async function runEnrich(argv) {
|
|
409
616
|
const sub = argv[0];
|
|
410
617
|
const rest = argv.slice(1);
|
|
@@ -419,6 +626,8 @@ async function runEnrich(argv) {
|
|
|
419
626
|
return runEnrichBrief(rest);
|
|
420
627
|
case "ingest":
|
|
421
628
|
return runEnrichIngest(rest);
|
|
629
|
+
case "materialize":
|
|
630
|
+
return runEnrichMaterialize(rest);
|
|
422
631
|
default:
|
|
423
632
|
console.error(`unknown \`mla enrich\` subcommand: ${sub}\n`);
|
|
424
633
|
console.error(USAGE);
|
|
@@ -64,6 +64,7 @@ const config_1 = require("../lib/config");
|
|
|
64
64
|
const active_memory_1 = require("../lib/active-memory");
|
|
65
65
|
const auto_index_1 = require("../lib/auto-index");
|
|
66
66
|
const kb_acl_1 = require("../lib/kb_acl");
|
|
67
|
+
const live_collector_1 = require("../lib/agent-memory-capture/live-collector");
|
|
67
68
|
const kb_add_1 = require("./kb_add");
|
|
68
69
|
function activeMemoryStorePath() {
|
|
69
70
|
return path.join(config_1.HOME, "logs", "kb-knowledge.jsonl");
|
|
@@ -87,6 +88,50 @@ function parseArgs(argv) {
|
|
|
87
88
|
}
|
|
88
89
|
return { sessionId };
|
|
89
90
|
}
|
|
91
|
+
// Roll the per-binding live results into a flat outcome tally for the worker's
|
|
92
|
+
// JSON summary. Counts only; never carries content. `locked` is the number of
|
|
93
|
+
// bindings skipped because another collector held the lock this pass.
|
|
94
|
+
function tallyLive(results) {
|
|
95
|
+
let bindings = 0;
|
|
96
|
+
let uploaded = 0;
|
|
97
|
+
let deferred = 0;
|
|
98
|
+
let blocked = 0;
|
|
99
|
+
let withdrawn = 0;
|
|
100
|
+
let failed = 0;
|
|
101
|
+
// Bindings that did no work this pass: another collector held the lock, or the
|
|
102
|
+
// pipeline threw (fail-soft). Both surface as summary === null.
|
|
103
|
+
let skippedBindings = 0;
|
|
104
|
+
for (const r of results) {
|
|
105
|
+
if (!r.locked || !r.summary) {
|
|
106
|
+
skippedBindings++;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
bindings++;
|
|
110
|
+
for (const rec of r.summary.records) {
|
|
111
|
+
switch (rec.outcome) {
|
|
112
|
+
case "uploaded":
|
|
113
|
+
uploaded++;
|
|
114
|
+
break;
|
|
115
|
+
case "deferred":
|
|
116
|
+
deferred++;
|
|
117
|
+
break;
|
|
118
|
+
case "blocked":
|
|
119
|
+
blocked++;
|
|
120
|
+
break;
|
|
121
|
+
case "reclassified":
|
|
122
|
+
case "deleted":
|
|
123
|
+
withdrawn++;
|
|
124
|
+
break;
|
|
125
|
+
case "failed":
|
|
126
|
+
failed++;
|
|
127
|
+
break;
|
|
128
|
+
default:
|
|
129
|
+
break; // unchanged / skipped: no-op, not surfaced
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return { bindings, uploaded, deferred, blocked, withdrawn, failed, skippedBindings };
|
|
134
|
+
}
|
|
90
135
|
// runKbAdd converts KbOwnerCheckError into stderr + exit 2 before it reaches
|
|
91
136
|
// this loop, so a thrown denial only arrives from injected add fns or future
|
|
92
137
|
// boundary changes. Match the class, the name (survives module-duplication
|
|
@@ -177,7 +222,25 @@ async function runInternalAutoIndex(argv, deps = {}) {
|
|
|
177
222
|
failed++; // fail-soft: one bad add never aborts the batch.
|
|
178
223
|
}
|
|
179
224
|
}
|
|
180
|
-
|
|
225
|
+
// Live agent-memory capture (proposal §6): the collector attached to this
|
|
226
|
+
// existing Stop worker. DEFAULT OFF: it runs only when the operator opted in
|
|
227
|
+
// (MEETLESS_AGENT_MEMORY_LIVE) AND there is a consented binding + actor.
|
|
228
|
+
// Fully fail-soft and OUTSIDE the Zone-2 result above: any error here is
|
|
229
|
+
// swallowed and never changes the indexed/skipped/failed counts or the
|
|
230
|
+
// session. Runs last so it rides the same detached tail.
|
|
231
|
+
const summary = { indexed, skipped, failed, total: targets.length };
|
|
232
|
+
const liveEnabled = deps.liveEnabled ?? live_collector_1.liveCaptureEnabled;
|
|
233
|
+
if (liveEnabled()) {
|
|
234
|
+
try {
|
|
235
|
+
const runLive = deps.runLive ?? live_collector_1.runLiveCollector;
|
|
236
|
+
const liveResults = await runLive({ nowIso: new Date().toISOString() });
|
|
237
|
+
summary.liveCapture = tallyLive(liveResults);
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// Live capture must never disturb the auto-index worker. Swallow.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
console.log(JSON.stringify(summary));
|
|
181
244
|
return 0;
|
|
182
245
|
}
|
|
183
246
|
catch {
|
|
@@ -18,6 +18,39 @@
|
|
|
18
18
|
// `mla`) would block the tool; we must never do that. Every failure path -- unreadable stdin,
|
|
19
19
|
// malformed payload, store open failure, a throwing dependency -- fails OPEN to the exit-0
|
|
20
20
|
// pass-through. The decision travels in the JSON body, never the exit code.
|
|
21
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
22
|
+
if (k2 === undefined) k2 = k;
|
|
23
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
24
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
25
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
26
|
+
}
|
|
27
|
+
Object.defineProperty(o, k2, desc);
|
|
28
|
+
}) : (function(o, m, k, k2) {
|
|
29
|
+
if (k2 === undefined) k2 = k;
|
|
30
|
+
o[k2] = m[k];
|
|
31
|
+
}));
|
|
32
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
33
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
34
|
+
}) : function(o, v) {
|
|
35
|
+
o["default"] = v;
|
|
36
|
+
});
|
|
37
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
38
|
+
var ownKeys = function(o) {
|
|
39
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
40
|
+
var ar = [];
|
|
41
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
42
|
+
return ar;
|
|
43
|
+
};
|
|
44
|
+
return ownKeys(o);
|
|
45
|
+
};
|
|
46
|
+
return function (mod) {
|
|
47
|
+
if (mod && mod.__esModule) return mod;
|
|
48
|
+
var result = {};
|
|
49
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
50
|
+
__setModuleDefault(result, mod);
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
})();
|
|
21
54
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
55
|
exports.PRETOOL_PASS_THROUGH = void 0;
|
|
23
56
|
exports.renderPreToolUseResponse = renderPreToolUseResponse;
|
|
@@ -28,6 +61,7 @@ const active_conflict_cache_1 = require("../lib/active-conflict-cache");
|
|
|
28
61
|
const ce0_store_1 = require("../lib/rules/ce0-store");
|
|
29
62
|
const enforce_notes_version_1 = require("../lib/rules/enforce-notes-version");
|
|
30
63
|
const observe_adapter_1 = require("../lib/rules/observe-adapter");
|
|
64
|
+
const enforcement_classify_1 = require("../lib/analytics/enforcement-classify");
|
|
31
65
|
const runtime_scope_1 = require("../lib/rules/runtime-scope");
|
|
32
66
|
const live_input_authority_1 = require("../lib/rules/live-input-authority");
|
|
33
67
|
const cache_1 = require("../lib/scanner/cache");
|
|
@@ -120,7 +154,7 @@ async function computePretoolDecision(raw, deps) {
|
|
|
120
154
|
const clock = (deps.clock ?? defaultClock)();
|
|
121
155
|
const resolveInputAuthority = deps.resolveInputAuthority ?? live_input_authority_1.resolveLiveInputAuthority;
|
|
122
156
|
const directives = (deps.resolveDirectives ?? defaultResolveDirectives)();
|
|
123
|
-
const { response } = await (0, enforce_notes_version_1.evaluateEnforceOrObserveNotesRule)(store, {
|
|
157
|
+
const { response, outcome } = await (0, enforce_notes_version_1.evaluateEnforceOrObserveNotesRule)(store, {
|
|
124
158
|
rawStdin: raw,
|
|
125
159
|
runtimeProjectRoot: scope.runtimeProjectRoot,
|
|
126
160
|
runtimeScopeId: scope.runtimeScopeId,
|
|
@@ -135,6 +169,10 @@ async function computePretoolDecision(raw, deps) {
|
|
|
135
169
|
// passes through do we layer the SOFT cross-session conflict warning (which can
|
|
136
170
|
// only ever advise, never deny).
|
|
137
171
|
if ("permissionDecision" in response && response.permissionDecision === "deny") {
|
|
172
|
+
// Emit the one analytics append the deny tile reads (§5.1). Awaited so the
|
|
173
|
+
// synchronous local spool write lands before this short-lived process exits;
|
|
174
|
+
// fail-soft so a telemetry fault can never turn into a thrown (blocking) hook.
|
|
175
|
+
await emitDenyIncident(raw, outcome, clock.now, deps);
|
|
138
176
|
return renderPreToolUseResponse(response);
|
|
139
177
|
}
|
|
140
178
|
return computeConflictWarning(raw, deps);
|
|
@@ -185,6 +223,53 @@ function computeConflictWarning(raw, deps) {
|
|
|
185
223
|
return renderPreToolUseResponse(exports.PRETOOL_PASS_THROUGH);
|
|
186
224
|
}
|
|
187
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Emit the enforcement-incident analytics event for one fired deny (the deny tile, §5.1).
|
|
228
|
+
* Classifies the tool + blocked-path surface into PII-safe enums (the path never leaves the
|
|
229
|
+
* device), resolves the workspace fail-open, and hands the built input/coords to the injected
|
|
230
|
+
* emitter (production lazy-imports the real append-only emit). The whole thing is fail-soft: a
|
|
231
|
+
* deny must never be turned into a thrown, blocking hook by a telemetry fault. The emitter is
|
|
232
|
+
* awaited so its synchronous local append lands before this short-lived hook process exits.
|
|
233
|
+
*/
|
|
234
|
+
async function emitDenyIncident(raw, outcome, nowMs, deps) {
|
|
235
|
+
try {
|
|
236
|
+
// The deny response is only ever returned alongside a DENIED outcome (the enforce path);
|
|
237
|
+
// guard defensively so a future shape change degrades to no-telemetry, never a wrong event.
|
|
238
|
+
if (outcome.kind !== "DENIED")
|
|
239
|
+
return;
|
|
240
|
+
const parsed = (0, observe_adapter_1.parsePreToolUseInput)(raw);
|
|
241
|
+
const filePath = typeof parsed?.tool_input?.file_path === "string" ? parsed.tool_input.file_path : null;
|
|
242
|
+
let workspaceId = null;
|
|
243
|
+
try {
|
|
244
|
+
workspaceId = (0, workspace_1.resolveWorkspaceIdWithEnv)() || null;
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
workspaceId = null;
|
|
248
|
+
}
|
|
249
|
+
const input = {
|
|
250
|
+
incidentId: outcome.attemptId,
|
|
251
|
+
decision: "deny",
|
|
252
|
+
tool: (0, enforcement_classify_1.normalizeEnforcedTool)(parsed?.tool_name),
|
|
253
|
+
touchedSurface: (0, enforcement_classify_1.classifyTouchedSurface)(filePath),
|
|
254
|
+
ruleVersionId: outcome.ruleVersionId,
|
|
255
|
+
};
|
|
256
|
+
const coords = {
|
|
257
|
+
workspaceId,
|
|
258
|
+
sessionId: parsed?.session_id ?? null,
|
|
259
|
+
nowMs,
|
|
260
|
+
};
|
|
261
|
+
await (deps.emitIncident ?? defaultEmitIncident)(input, coords);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
// Fail-soft: deny telemetry must never escalate into a blocking hook.
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/** Production deny emitter: lazy-imports the recorder-touching emit so the non-deny hot path
|
|
268
|
+
* never loads it, then performs the synchronous local append. Awaited by the caller. */
|
|
269
|
+
async function defaultEmitIncident(input, coords) {
|
|
270
|
+
const { emitEnforcementIncident } = await Promise.resolve().then(() => __importStar(require("../lib/analytics/enforcement-incident")));
|
|
271
|
+
emitEnforcementIncident(input, coords);
|
|
272
|
+
}
|
|
188
273
|
function defaultResolveScope() {
|
|
189
274
|
const root = (0, runtime_scope_1.resolveActiveRuntimeScopeId)();
|
|
190
275
|
return { runtimeScopeId: root, runtimeProjectRoot: root };
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// `mla _internal redact-capture` (governed-story capture, P1). The
|
|
3
|
+
// user-prompt-submit / post-tool-use hooks assemble injected-context blocks and
|
|
4
|
+
// MCP query text in shell, then pipe a single JSON payload through this command
|
|
5
|
+
// to redact secrets BEFORE the trace is spooled. We redact here (in a node
|
|
6
|
+
// child) rather than in bash so the capture path reuses the ONE parity-locked
|
|
7
|
+
// redactor (lib/redactor.ts, mirror of intel + control), instead of forking a
|
|
8
|
+
// third, drifting copy of the secret patterns into shell. Spec
|
|
9
|
+
// notes/20260627-session-detail-mla-actions-and-colored-injection-timeline-design.md §4.4.
|
|
10
|
+
//
|
|
11
|
+
// Contract (fail-closed telemetry, fail-open agent -- the agent's prompt is
|
|
12
|
+
// delivered by the hook independently of this call):
|
|
13
|
+
// stdin : { blocks?: [{kind, content, citations?, charCount?, itemCount?}], query?: string }
|
|
14
|
+
// stdout : { blocks: [{kind, content, contentStatus, citations, charCount, itemCount}], query }
|
|
15
|
+
// exit 0 : redaction succeeded; the hook spools the redacted output verbatim.
|
|
16
|
+
// exit 1 : ANY failure (unreadable/malformed stdin, serialization fault). The
|
|
17
|
+
// hook treats this as redaction_failed: it persists content:null +
|
|
18
|
+
// contentStatus:"redaction_failed" for every block and NEVER
|
|
19
|
+
// substitutes a raw body. Raw content never leaves this process on a
|
|
20
|
+
// failure path.
|
|
21
|
+
//
|
|
22
|
+
// charCount is computed HERE from the raw (pre-redaction) body so it is a single
|
|
23
|
+
// factual source the control boundary can check (summary.injectedCharCount ==
|
|
24
|
+
// sum(block.charCount)); the producer cannot drift it. citations and itemCount
|
|
25
|
+
// are producer metadata and pass through untouched (they are governance ids /
|
|
26
|
+
// counts, not secrets).
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.redactCapturePayload = redactCapturePayload;
|
|
29
|
+
exports.runInternalRedactCapture = runInternalRedactCapture;
|
|
30
|
+
const redactor_1 = require("../lib/redactor");
|
|
31
|
+
function asStringArray(value) {
|
|
32
|
+
if (!Array.isArray(value))
|
|
33
|
+
return [];
|
|
34
|
+
return value.filter((v) => typeof v === "string");
|
|
35
|
+
}
|
|
36
|
+
function asNonNegativeInt(value) {
|
|
37
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
// Count code points (not UTF-16 units) so the "N chars" the chip later shows is
|
|
43
|
+
// honest for multi-byte content. Computed from the RAW body, pre-redaction.
|
|
44
|
+
function charCountOf(content) {
|
|
45
|
+
return Array.from(content).length;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Pure. Redact every block body and the query with the shared redactor.
|
|
49
|
+
* - content null/empty stays as-is, contentStatus "available" (nothing to scrub).
|
|
50
|
+
* - content that the redactor changed -> "redacted"; unchanged -> "available".
|
|
51
|
+
* - charCount is the ORIGINAL (pre-redaction) length, factual.
|
|
52
|
+
* Never throws on well-formed string inputs (redact is a regex replace); the IO
|
|
53
|
+
* shell maps a throw to exit 1.
|
|
54
|
+
*/
|
|
55
|
+
function redactCapturePayload(input) {
|
|
56
|
+
const rawBlocks = Array.isArray(input.blocks) ? input.blocks : [];
|
|
57
|
+
const blocks = rawBlocks.map((b) => {
|
|
58
|
+
const block = (b ?? {});
|
|
59
|
+
const kind = typeof block.kind === "string" ? block.kind : "unknown";
|
|
60
|
+
const citations = asStringArray(block.citations);
|
|
61
|
+
const itemCount = asNonNegativeInt(block.itemCount);
|
|
62
|
+
const rawContent = typeof block.content === "string" ? block.content : null;
|
|
63
|
+
if (rawContent === null || rawContent === "") {
|
|
64
|
+
return {
|
|
65
|
+
kind,
|
|
66
|
+
content: rawContent,
|
|
67
|
+
contentStatus: "available",
|
|
68
|
+
citations,
|
|
69
|
+
charCount: 0,
|
|
70
|
+
itemCount,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const redacted = (0, redactor_1.redact)(rawContent) ?? rawContent;
|
|
74
|
+
return {
|
|
75
|
+
kind,
|
|
76
|
+
content: redacted,
|
|
77
|
+
contentStatus: redacted === rawContent ? "available" : "redacted",
|
|
78
|
+
citations,
|
|
79
|
+
charCount: charCountOf(rawContent),
|
|
80
|
+
itemCount,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
const rawQuery = typeof input.query === "string" ? input.query : null;
|
|
84
|
+
const query = rawQuery === null ? null : ((0, redactor_1.redact)(rawQuery) ?? rawQuery);
|
|
85
|
+
return { blocks, query };
|
|
86
|
+
}
|
|
87
|
+
function readStdinReal() {
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const chunks = [];
|
|
90
|
+
process.stdin.on("data", (c) => chunks.push(c));
|
|
91
|
+
process.stdin.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
92
|
+
process.stdin.on("error", reject);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const defaultDeps = {
|
|
96
|
+
readStdin: readStdinReal,
|
|
97
|
+
writeOut: (s) => process.stdout.write(s),
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* IO shell. Reads the JSON payload from stdin, redacts, writes the redacted JSON
|
|
101
|
+
* to stdout. Exit 1 on ANY failure (read error, malformed JSON, serialization
|
|
102
|
+
* fault) WITHOUT writing a partial/raw body, so the hook degrades to
|
|
103
|
+
* redaction_failed and never persists an unredacted secret. Takes no argv.
|
|
104
|
+
*/
|
|
105
|
+
async function runInternalRedactCapture(_argv, deps = defaultDeps) {
|
|
106
|
+
let raw;
|
|
107
|
+
try {
|
|
108
|
+
raw = await deps.readStdin();
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = JSON.parse(raw);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return 1;
|
|
119
|
+
}
|
|
120
|
+
if (!parsed || typeof parsed !== "object")
|
|
121
|
+
return 1;
|
|
122
|
+
try {
|
|
123
|
+
const out = redactCapturePayload(parsed);
|
|
124
|
+
deps.writeOut(JSON.stringify(out));
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
return 1;
|
|
129
|
+
}
|
|
130
|
+
}
|