@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/CHANGELOG.md +48 -1
- package/dist/audit.d.ts +70 -3
- package/dist/audit.js +664 -82
- package/dist/cli.js +54 -5
- package/dist/fleet-health.d.ts +23 -0
- package/dist/fleet-health.js +56 -0
- package/dist/http-server.js +12 -2
- package/dist/index.js +41 -3
- package/dist/secret-shapes.js +7 -4
- package/package.json +1 -1
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
|
-
|
|
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(`
|
|
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
|
}
|
package/dist/fleet-health.d.ts
CHANGED
|
@@ -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;
|
package/dist/fleet-health.js
CHANGED
|
@@ -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}`);
|
package/dist/http-server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/dist/secret-shapes.js
CHANGED
|
@@ -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
|
|
54
|
-
//
|
|
55
|
-
//
|
|
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)|
|
|
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.
|
|
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",
|