@compr/opscontext-mcp 2.10.0 → 2.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -651,7 +651,7 @@ import { listLearnings, parseSince, learningsToChunks, learningsStats, formatLea
651
651
  import { saveSession, loadSession, listSessions, deleteSession, formatSession, formatSessionList, } from "./sessions.js";
652
652
  import { activate, deactivate, getActivationStatus, gateCheckFresh, } from "./activation.js";
653
653
  import { syncTierA, syncTierB, loadCommunityStore, communityRulesToChunks, mergeWithDedup, STORE_PATH as COMMUNITY_STORE_PATH, } from "./community-sync.js";
654
- import { readAuditLog, verifyChain, filterByRange, toCsv, rotateAuditLog, planRotation, listSegments, acknowledgeRedaction, restoreSegment, scrubAuditLog, } from "./audit.js";
654
+ import { readAuditLog, verifyChain, filterByRange, toCsv, rotateAuditLog, planRotation, listSegments, acknowledgeRedaction, restoreSegment, scrubAuditLog, recordVerifyState, acquireVerifyLock, } from "./audit.js";
655
655
  import { redactPayload, redactChunk } from "./secret-shapes.js";
656
656
  import { loadRepoPolicy, parsePolicy, formatPolicySummary, formatValidationErrors, repoPolicyPath, } from "./policy.js";
657
657
  import { buildCostReport } from "./cost-report.js";
@@ -1751,7 +1751,16 @@ safeAppend), visible via 'contextengine audit-verify' and consumed by the
1751
1751
  console.error(`❌ ${refused}`);
1752
1752
  process.exit(1);
1753
1753
  }
1754
- safeAppend(eventKind, prepareCapturedPayload(payload, eventKind), actor);
1754
+ // [LOCKED] [RECEIVER-SAYS-WHAT-WAS-WRITTEN] - 2026-09-27
1755
+ // [NEVER] report "Appended" or exit 0 for an entry the audit log refused.
1756
+ // WHY: on a stuck log (a record cut short by a full disk) this printed "Appended vscode.tool_call to
1757
+ // audit log." and exited 0 while nothing landed (E2E_REVIEW_2026-09 B1-1); the VS Code
1758
+ // extension emits through this command and believed it.
1759
+ // FIX: safeAppend() returns whether it wrote; a refusal exits 1. [LOCK] [A-REFUSED-APPEND-IS-COUNTED-AND-CHAINED]
1760
+ if (!safeAppend(eventKind, prepareCapturedPayload(payload, eventKind), actor)) {
1761
+ console.error(`❌ Not recorded: the audit log refused ${eventKind} (reason above). The refusal is counted and will be noted on the chain.`);
1762
+ process.exit(1);
1763
+ }
1755
1764
  console.log(`✅ Appended ${eventKind} to audit log.`);
1756
1765
  }
1757
1766
  async function cliWatch(args) {
@@ -2129,11 +2138,35 @@ function cliAuditRedactAck(args) {
2129
2138
  console.log(` audit-verify now: ${after.ok ? "OK" : "FAILED"}, ${(after.redactedIndices ?? []).length} redacted, ${(after.tamperedIndices ?? []).length} altered.`);
2130
2139
  }
2131
2140
  async function cliAuditVerify() {
2141
+ // [LOCK] [HEALTH-SEES-THE-CHAIN]: every full check leaves its result for fleet health. The
2142
+ // daily one (--scheduled, started by the indexing server) runs one at a time and prints nothing.
2143
+ const scheduled = process.argv.includes("--scheduled");
2144
+ let releaseVerifyLock = null;
2145
+ if (scheduled) {
2146
+ releaseVerifyLock = acquireVerifyLock();
2147
+ if (!releaseVerifyLock)
2148
+ return; // another check is running
2149
+ }
2150
+ const t0 = Date.now();
2132
2151
  const report = verifyChain();
2152
+ recordVerifyState(report, Date.now() - t0, scheduled ? "scheduled" : "cli");
2153
+ if (scheduled) {
2154
+ releaseVerifyLock?.();
2155
+ return;
2156
+ }
2133
2157
  const forks = report.forkIndices ?? [];
2134
2158
  const redacted = report.redactedIndices ?? [];
2159
+ const dups = report.duplicateIndices ?? [];
2160
+ // [LOCK] [VERIFY-FORK-IS-NOT-TAMPER]: a second copy of a record is counted once and named as a copy.
2161
+ const copiesNote = () => {
2162
+ if (dups.length === 0)
2163
+ return;
2164
+ console.log(`\n⚠️ ${dups.length} record(s) appear twice (a second copy of a record already in the log).`);
2165
+ console.log(` Counted once. Content intact, nothing missing. Usually a log trim that was interrupted or`);
2166
+ console.log(` ran while entries arrived. At: ${dups.slice(0, 8).join(", ")}${dups.length > 8 ? `, … (+${dups.length - 8} more)` : ""}`);
2167
+ };
2135
2168
  if (report.ok) {
2136
- console.log(`✅ Audit chain verified — ${report.total} record(s).`);
2169
+ console.log(`✅ Audit chain verified — ${report.total - dups.length} record(s).`);
2137
2170
  console.log(redacted.length === 0
2138
2171
  ? ` No record was altered, and no history is missing.`
2139
2172
  : ` No history is missing. ${redacted.length} record(s) redacted and acknowledged on the chain (indices ${redacted.slice(0, 8).join(", ")}${redacted.length > 8 ? ", …" : ""}), 0 altered.`);
@@ -2146,6 +2179,7 @@ async function cliAuditVerify() {
2146
2179
  console.log(` property holds. Do NOT rewrite the log to linearise it — that would destroy`);
2147
2180
  console.log(` the evidence it exists to provide.`);
2148
2181
  }
2182
+ copiesNote();
2149
2183
  return;
2150
2184
  }
2151
2185
  console.error(`❌ Audit chain FAILED — ${report.total} record(s) checked.`);
@@ -2155,12 +2189,24 @@ async function cliAuditVerify() {
2155
2189
  console.error(`\n Altered records (content does not match its own hash):`);
2156
2190
  console.error(` ${t.slice(0, 10).join(", ")}${t.length > 10 ? `, … (+${t.length - 10} more)` : ""}`);
2157
2191
  console.error(` This is tampering: the record's bytes were changed after it was written.`);
2158
- console.error(` If this was a deliberate redaction of a secret, acknowledge it on the chain:`);
2159
- console.error(` contextengine audit-redact-ack --index ${t.slice(0, 3).join(",")} --reason "<what was removed and why>"`);
2192
+ console.error(` If this was a deliberate redaction of a secret, acknowledge it on the chain`);
2193
+ console.error(` (every altered index is listed, so the command can be pasted as it is):`);
2194
+ console.error(` contextengine audit-redact-ack --index ${t.join(",")} --reason "<what was removed and why>"`);
2160
2195
  }
2161
2196
  if (redacted.length > 0) {
2162
2197
  console.error(`\n Also ${redacted.length} redacted record(s), acknowledged on the chain, not counted above.`);
2163
2198
  }
2199
+ // [LOCK] [VERIFY-READS-PAST-AN-UNREADABLE-LINE]: say where, and that the rest was checked.
2200
+ const unreadable = report.unreadable ?? [];
2201
+ if (unreadable.length > 0) {
2202
+ console.error(`\n Lines that are not records (every other record was checked):`);
2203
+ for (const u of unreadable.slice(0, 10))
2204
+ console.error(` ${u.file} line ${u.line}`);
2205
+ if (unreadable.length > 10)
2206
+ console.error(` … (+${unreadable.length - 10} more)`);
2207
+ console.error(` A last record cut short by a full disk is set aside by the next append on its own;`);
2208
+ console.error(` a line like these is damage: keep a copy of the file before touching it.`);
2209
+ }
2164
2210
  if ((report.orphanIndices ?? []).length > 0) {
2165
2211
  const o = report.orphanIndices;
2166
2212
  console.error(`\n Orphaned records (parent hash absent from the log):`);
@@ -2172,6 +2218,9 @@ async function cliAuditVerify() {
2172
2218
  if (forks.length > 0) {
2173
2219
  console.error(`\n (Also ${forks.length} concurrent-append fork(s) — benign, see docs.)`);
2174
2220
  }
2221
+ if (dups.length > 0) {
2222
+ console.error(`\n (Also ${dups.length} second copies of records, counted once, benign.)`);
2223
+ }
2175
2224
  console.error(`\nFor compliance-graded evidence, treat the affected records as unverified.`);
2176
2225
  process.exit(2);
2177
2226
  }
@@ -45,6 +45,27 @@ export interface FleetHealth {
45
45
  detail: string;
46
46
  }>;
47
47
  };
48
+ /** The last full check of the audit chain (`audit-verify`, run by hand or daily by the indexing
49
+ * server), or null when it never ran here. [LOCK] [HEALTH-SEES-THE-CHAIN] */
50
+ chain: {
51
+ checkedAt: string;
52
+ ageHours: number;
53
+ ok: boolean;
54
+ unique: number;
55
+ altered: number;
56
+ orphans: number;
57
+ unreadable: number;
58
+ duplicates: number;
59
+ reason: string | null;
60
+ } | null;
61
+ /** Entries the audit log refused today (chained as audit.append_failed, or still pending), and
62
+ * records cut short today (audit.torn_tail). */
63
+ auditLog: {
64
+ refusedToday: number;
65
+ lastRefusal: string | null;
66
+ tornToday: number;
67
+ lastTornKept: string | null;
68
+ };
48
69
  /** The newest release for which verify-release passed on this machine, or null. */
49
70
  lastVerifiedRelease: string | null;
50
71
  /** Measured problems only. Empty means green. */
@@ -56,6 +77,8 @@ export declare const REINDEX_PER_HOUR_WARN = 30;
56
77
  export declare const DOUBLED_HOOK_EVENTS_WARN_PCT = 5;
57
78
  export declare const DOUBLED_MIN_EVENTS = 10;
58
79
  export declare const DOUBLED_WINDOW_MS = 2000;
80
+ /** A full check older than this is reported: the indexing server runs one a day. */
81
+ export declare const CHAIN_CHECK_STALE_HOURS = 48;
59
82
  export declare function fleetHealthPath(): string;
60
83
  /** The highest version with a verify-release marker; version order, not file time (ties). */
61
84
  export declare function lastVerifiedRelease(dir?: string): string | null;
@@ -13,6 +13,7 @@ import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readSync, readdi
13
13
  import { join } from "path";
14
14
  import { homedir } from "os";
15
15
  import { listServers } from "./server-registry.js";
16
+ import { readVerifyState, pendingRefusals } from "./audit.js";
16
17
  import { claudeHookRegistrations } from "./install-claude-hook.js";
17
18
  export const REINDEX_PER_HOUR_WARN = 30;
18
19
  /** Doubled hook events above this share of the day's hook events raise a warning, once the day
@@ -20,6 +21,8 @@ export const REINDEX_PER_HOUR_WARN = 30;
20
21
  export const DOUBLED_HOOK_EVENTS_WARN_PCT = 5;
21
22
  export const DOUBLED_MIN_EVENTS = 10;
22
23
  export const DOUBLED_WINDOW_MS = 2000;
24
+ /** A full check older than this is reported: the indexing server runs one a day. */
25
+ export const CHAIN_CHECK_STALE_HOURS = 48;
23
26
  const TAIL_BYTES = 8 * 1024 * 1024;
24
27
  function ceHome() {
25
28
  return process.env.CONTEXTENGINE_HOME || join(homedir(), ".contextengine");
@@ -109,6 +112,8 @@ export function computeFleetHealth(opts = {}) {
109
112
  let lastHourWrites = 0, blocks = 0, refusals = 0, learningsSaved = 0, hookEvents = 0, doubledHookEvents = 0;
110
113
  const lastBlocks = [];
111
114
  let prevHook = null;
115
+ let refusedToday = 0, tornToday = 0;
116
+ let lastRefusal = null, lastTornKept = null;
112
117
  for (const r of records) {
113
118
  const t = Date.parse(r.ts);
114
119
  if (Number.isNaN(t))
@@ -127,6 +132,16 @@ export function computeFleetHealth(opts = {}) {
127
132
  }
128
133
  if (t < midnight)
129
134
  continue;
135
+ if (r.event === "audit.append_failed") {
136
+ refusedToday += Number(r.payload?.count ?? 0) || 0;
137
+ const errs = r.payload?.errors;
138
+ if (errs && Object.keys(errs).length > 0)
139
+ lastRefusal = Object.keys(errs)[0];
140
+ }
141
+ if (r.event === "audit.torn_tail") {
142
+ tornToday++;
143
+ lastTornKept = String(r.payload?.kept ?? "");
144
+ }
130
145
  if (r.event === "hook.block") {
131
146
  blocks++;
132
147
  lastBlocks.push({ ts: r.ts, kind: "pre-commit", detail: blockDetail(r.payload) });
@@ -161,6 +176,42 @@ export function computeFleetHealth(opts = {}) {
161
176
  warnings.push(`${lastHourWrites} shared-index writes in the last hour (ceiling ${REINDEX_PER_HOUR_WARN}): something saves in a loop`);
162
177
  if (refusals > 0)
163
178
  warnings.push(`${refusals} learnings-store refusal(s) today: a write looked like a wipe or a runaway import`);
179
+ // [LOCKED] [HEALTH-SEES-THE-CHAIN] - 2026-09-27
180
+ // [NEVER] report green while the audit chain failed its last check, or while the log refuses entries.
181
+ // WHY: fleet health read the audit log for counts only. On three broken logs (a stuck log whose
182
+ // appends were all refused, missing history, 100 altered records) the verifier said FAILED
183
+ // and health said 0 warnings (E2E_REVIEW_2026-09 B6-1). The chain is the product's claim.
184
+ // FIX: health never runs the check (26 s and 3.5 GB on 4.9M records, measured): it reads the
185
+ // result every `audit-verify` leaves, which the indexing server refreshes daily in a separate
186
+ // low-priority process. A failed check, a check older than CHAIN_CHECK_STALE_HOURS, refused
187
+ // entries (chained as audit.append_failed or still pending) and records cut short today are
188
+ // each a warning. [LOCK] [A-REFUSED-APPEND-IS-COUNTED-AND-CHAINED] [LOCK] [TORN-TAIL-IS-KEPT-AND-CHAINED]
189
+ const vs = readVerifyState();
190
+ const chain = vs
191
+ ? {
192
+ checkedAt: vs.checkedAt,
193
+ ageHours: Math.max(0, Math.round((now.getTime() - Date.parse(vs.checkedAt)) / 3_600_000)),
194
+ ok: vs.ok,
195
+ unique: vs.unique,
196
+ altered: vs.altered,
197
+ orphans: vs.orphans,
198
+ unreadable: vs.unreadable,
199
+ duplicates: vs.duplicates,
200
+ reason: vs.reason,
201
+ }
202
+ : null;
203
+ if (chain && !chain.ok)
204
+ warnings.push(`the audit chain did not verify at its last check (${chain.checkedAt.slice(0, 16).replace("T", " ")}Z): ${chain.reason ?? "see contextengine audit-verify"}`);
205
+ else if (chain && chain.ageHours > CHAIN_CHECK_STALE_HOURS)
206
+ warnings.push(`the audit chain was last checked ${chain.ageHours} h ago: run contextengine audit-verify`);
207
+ const pending = pendingRefusals();
208
+ if (pending.count > 0 && pending.error)
209
+ lastRefusal = pending.error; // the newest refusal wins
210
+ const refusedTotal = refusedToday + pending.count;
211
+ if (refusedTotal > 0)
212
+ warnings.push(`the audit log refused ${refusedTotal} entr${refusedTotal === 1 ? "y" : "ies"} today${lastRefusal ? ` (${lastRefusal})` : ""}: they are not in the log, and the gap is noted on the chain`);
213
+ if (tornToday > 0)
214
+ warnings.push(`${tornToday} audit record(s) were cut short today (a full disk?); the bytes are kept in ${lastTornKept}`);
164
215
  for (const w of report.warnings)
165
216
  if (/index on their own/.test(w))
166
217
  warnings.push(w);
@@ -172,6 +223,8 @@ export function computeFleetHealth(opts = {}) {
172
223
  reindex: { lastHourWrites, perCorpus, threshold: REINDEX_PER_HOUR_WARN },
173
224
  claudeHooks,
174
225
  today: { hookEvents, doubledHookEvents, blocks, refusals, learningsSaved, lastBlocks: lastBlocks.slice(-3) },
226
+ chain,
227
+ auditLog: { refusedToday: refusedTotal, lastRefusal, tornToday, lastTornKept },
175
228
  lastVerifiedRelease: lastVerifiedRelease(),
176
229
  warnings,
177
230
  };
@@ -191,6 +244,9 @@ export function formatFleetHealth(h) {
191
244
  lines.push(` servers ${h.servers.total}: ${h.servers.indexers} indexing, ${h.servers.readers} reading, ${h.servers.stale.length} on an old build`);
192
245
  lines.push(` shared-index writes last hour: ${h.reindex.lastHourWrites} (ceiling ${h.reindex.threshold})`);
193
246
  lines.push(` today: ${h.today.blocks} block(s) prevented, ${h.today.refusals} store refusal(s), ${h.today.learningsSaved} learning(s) saved`);
247
+ lines.push(h.chain
248
+ ? ` audit chain: ${h.chain.ok ? "verified" : "FAILED"} ${h.chain.ageHours} h ago, ${h.chain.unique} record(s)${h.chain.duplicates ? `, ${h.chain.duplicates} copies counted once` : ""}; ${h.auditLog?.refusedToday ?? 0} entr(ies) refused today`
249
+ : ` audit chain: not checked on this machine yet (the indexing server runs a full check daily; or run contextengine audit-verify)`);
194
250
  lines.push(` claude code: ${h.today.hookEvents} hook event(s) today, ${h.today.doubledHookEvents} doubled; registrations ${h.claudeHooks ? Object.entries(h.claudeHooks).map(([ev, n]) => `${ev}=${n}`).join(" ") : "no settings.json"}`);
195
251
  for (const b of h.today.lastBlocks)
196
252
  lines.push(` ${b.ts.slice(11, 19)}Z ${b.kind}: ${b.detail}`);
@@ -266,13 +266,23 @@ function handleEvents(req, res) {
266
266
  return;
267
267
  }
268
268
  // All valid — write them to audit log via safeAppend.
269
+ // [LOCK] [RECEIVER-SAYS-WHAT-WAS-WRITTEN] (src/cli.ts emit-event): count what the log took, and
270
+ // answer 503 when it refused any. On 2026-09-27 a stuck log answered {"ok":true,"written":25}
271
+ // with 0 of 25 recorded (E2E_REVIEW_2026-09 B1-1).
269
272
  let written = 0;
273
+ let failed = 0;
270
274
  for (const ev of batch.events) {
271
275
  const actor = typeof ev.actor === "string" ? ev.actor : "browser-ext";
272
276
  // event/payload were validated above — cast is safe.
273
277
  // [LOCK] [CAPTURE-IS-REDACTED-AT-THE-DOOR]: redact before the append, never after.
274
- safeAppend(ev.event, prepareCapturedPayload(ev.payload, ev.event), actor);
275
- written++;
278
+ if (safeAppend(ev.event, prepareCapturedPayload(ev.payload, ev.event), actor))
279
+ written++;
280
+ else
281
+ failed++;
282
+ }
283
+ if (failed > 0) {
284
+ sendJson(req, res, 503, { ok: false, error: "audit_append_failed", written, failed });
285
+ return;
276
286
  }
277
287
  sendJson(req, res, 200, { ok: true, written });
278
288
  });
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { loadEmbeddingStore, compactEmbeddingStore } from "./embedding-store.js"
13
13
  import { sharedIndexEnabled, corpusId, electIndexer, writeSharedIndex, readSharedIndex, sharedIndexMtime, } from "./shared-index.js";
14
14
  import { listProjects, checkPorts, runComplianceAudit, formatProjectList, formatPortMap, formatPlan, scoreProject, formatScoreReport, runScoreCanary, } from "./agents.js";
15
15
  import { saveSession, loadSession, listSessions, deleteSession, formatSession, formatSessionList, } from "./sessions.js";
16
- import { verifyChain, readAuditLog, filterByRange, autoRotateAuditLog, safeAppend } from "./audit.js";
16
+ import { verifyChain, readAuditLog, filterByRange, autoRotateAuditLog, safeAppend, readVerifyState } from "./audit.js";
17
17
  import { registerServer, listServers, formatServers, liveDaemonPid } from "./server-registry.js";
18
18
  import { secureCeHome } from "./ce-home.js";
19
19
  import { QUOTED_TEXT_NOTE } from "./framing.js";
@@ -26,7 +26,8 @@ import { communityRulesToChunks, mergeWithDedup, loadCommunityStore, } from "./c
26
26
  import { readFileSync, existsSync, watch, statSync, writeFileSync, mkdirSync } from "fs";
27
27
  import { basename, join, dirname } from "path";
28
28
  import { homedir } from "os";
29
- import { execSync } from "child_process";
29
+ import { execSync, spawn } from "child_process";
30
+ import { setPriority } from "os";
30
31
  import { scanCodeDir } from "./code-chunker.js";
31
32
  import { fileURLToPath } from "url";
32
33
  import { TOOL_COUNT, FREE_TOOL_COUNT, PREMIUM_TOOL_NAMES } from "./tools-manifest.js";
@@ -460,6 +461,42 @@ function evaluateRole(reason) {
460
461
  }
461
462
  let healthTick = 0;
462
463
  let lastStaleCount = -1;
464
+ const SERVER_STARTED_AT = Date.now();
465
+ let lastChainCheckSpawn = 0;
466
+ /**
467
+ * The indexing server starts the daily full check of the audit chain, in its own process.
468
+ * [LOCK] [HEALTH-SEES-THE-CHAIN] (src/fleet-health.ts): the check costs 26 s and 3.5 GB on 4.9M
469
+ * records (measured 2026-09-27), so it never runs in this long-lived process, whose event loop
470
+ * serves the receiver and the tools. A separate low-priority `audit-verify --scheduled`, at most
471
+ * one attempt an hour, never in the first 10 minutes after a start, one at a time across processes
472
+ * (its own lock). CONTEXTENGINE_CHAIN_CHECK=0 turns it off.
473
+ */
474
+ function maybeScheduleChainCheck() {
475
+ if (role !== "indexer" || process.env.CONTEXTENGINE_CHAIN_CHECK === "0")
476
+ return;
477
+ const now = Date.now();
478
+ if (now - SERVER_STARTED_AT < 10 * 60_000 || now - lastChainCheckSpawn < 60 * 60_000)
479
+ return;
480
+ const state = readVerifyState();
481
+ if (state && now - Date.parse(state.checkedAt) < 24 * 3_600_000)
482
+ return;
483
+ lastChainCheckSpawn = now;
484
+ try {
485
+ const cli = join(dirname(fileURLToPath(import.meta.url)), "cli.js");
486
+ const child = spawn(process.execPath, [cli, "audit-verify", "--scheduled"], { stdio: "ignore", detached: true, env: process.env });
487
+ if (child.pid) {
488
+ try {
489
+ setPriority(child.pid, 10);
490
+ }
491
+ catch { /* the check still runs, at normal priority */ }
492
+ }
493
+ child.unref();
494
+ console.error(`[ContextEngine] 🔎 daily audit chain check started (pid ${child.pid ?? "?"})`);
495
+ }
496
+ catch (err) {
497
+ console.error(`[ContextEngine] ⚠ could not start the daily audit chain check: ${err.message}`);
498
+ }
499
+ }
463
500
  /**
464
501
  * The indexer writes ~/.contextengine/fleet-health.json once a minute: version drift, reindex
465
502
  * rate, today's blocks and refusals, the last verified release. Every surface reads that file.
@@ -475,6 +512,7 @@ function publishHealth() {
475
512
  }
476
513
  if (role === "indexer")
477
514
  writeFleetHealth(h);
515
+ maybeScheduleChainCheck();
478
516
  }
479
517
  catch (err) {
480
518
  console.error(`[ContextEngine] ⚠ fleet health failed: ${err.message}`);
@@ -1423,7 +1461,7 @@ async function main() {
1423
1461
  const runAutoRotate = () => {
1424
1462
  try {
1425
1463
  const o = autoRotateAuditLog();
1426
- if (o.action === "rotated" || o.action === "refused" || o.action === "error" || o.action === "in_progress") {
1464
+ if (o.action === "rotated" || o.action === "refused" || o.action === "error" || o.action === "in_progress" || o.action === "finished") {
1427
1465
  console.error(`[ContextEngine] 📦 audit auto-rotate (${o.action}): ${o.detail}`);
1428
1466
  }
1429
1467
  }
@@ -50,13 +50,16 @@ export const SECRET_SHAPES = [
50
50
  { id: "api_key_header", re: /(\bx-api-key:\s*)[^\s'"]{8,}/gi, keepPrefix: true },
51
51
  // name = value, the long tail. Skips variables, env lookups, paths, placeholders already
52
52
  // redacted, type names and function calls.
53
- // A name that is PASS alone or ends in a separator plus PASS (SMTP_PASS, DB_PASS, db.pass) counts
54
- // too: a real mail password sat in the shared index under SMTP_PASS (E2E_REVIEW_2026-09 A6-3).
55
- // The separator keeps bypass and compass out.
53
+ // A name that ends in a separator plus PASS (SMTP_PASS, DB_PASS, db.pass) counts too: a real mail
54
+ // password sat in the shared index under SMTP_PASS (E2E_REVIEW_2026-09 A6-3). The separator keeps
55
+ // bypass and compass out. A bare `pass` counts only before an equals sign, or before a quoted value
56
+ // after a colon (nodemailer's auth block): as first shipped in 2.10.0 it also took prose (a README's
57
+ // "PII pass" list) and code (a count of passing checks in agents.ts), which the public-release scan
58
+ // refused.
56
59
  {
57
60
  id: "credential_assignment",
58
61
  // A backtick opens a value too: "password: `...`" in Markdown, found in the real log.
59
- re: /(\b(?:[\w.-]*(?:password|passwd|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|private[_-]?key|client[_-]?secret)|(?:[\w.-]*[_.-])?pass)["'`]?\s*[:=]\s*["'`]?)(?![$<{*/~.[]|process\.env|os\.environ|getenv)[^\s'"`,;)}\]]{6,}/gi,
62
+ re: /(\b(?:[\w.-]*(?:password|passwd|pwd|secret|token|api[_-]?key|apikey|access[_-]?key|private[_-]?key|client[_-]?secret)|[\w.-]*[_.-]pass|pass(?=["'`]?\s*=|["'`]?\s*:\s*["'`]))["'`]?\s*[:=]\s*["'`]?)(?![$<{*/~.[]|process\.env|os\.environ|getenv)[^\s'"`,;)}\]]{6,}/gi,
60
63
  keepPrefix: true,
61
64
  skip: (value, after) => after.startsWith("(") ||
62
65
  (/^[A-Za-z_$][\w$.]*\(/.test(value) && after.startsWith(")")) || // getToken(user), not a value
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compr/opscontext-mcp",
3
- "version": "2.10.0",
3
+ "version": "2.11.0",
4
4
  "description": "OpsContext for AI Agents — read-only fleet visibility (PM2/nginx/Docker/git/cron) + tamper-evident audit log + policy-as-code hooks. The ops + compliance layer Claude Code can't grow natively.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",