fix: fail closed for diagnostic history reads

Require the decrypted configuration and its complete secret set before loading or returning any diagnostic history data. Propagate configuration failures so the agent response boundary falls back to its generic safe error instead of emitting unredacted history content.

Reject thrown and non-array dedicated history reader failures as unhealthy operations without consulting stale config history. Preserve redaction of reader errors at the agent boundary.

Keep caller-owned history snapshots unchanged by isolating the per-hoster summarizer from the reader array, and cover get_history, list_errors, and server_health with focused red-green regressions.
This commit is contained in:
Sucukdeluxe
2026-08-13 22:48:21 +02:00
parent f03e0c8c22
commit 1c8e9067ee
2 changed files with 110 additions and 33 deletions
+20 -14
View File
@@ -16,18 +16,24 @@ function createCollectors(deps) {
const { loadConfig, loadHistory, getAllLogPaths, support, stats, appInfo, systemInfo, agentInfo } = deps;
function _secrets() {
try { return support.collectSecretValues(loadConfig()); } catch { return []; }
return support.collectSecretValues(loadConfig());
}
function _currentHistory() {
if (typeof loadHistory === 'function') {
const history = loadHistory();
return Array.isArray(history) ? history : [];
if (!Array.isArray(history)) throw new Error('History reader returned invalid data');
return history;
}
const cfg = loadConfig();
return Array.isArray(cfg && cfg.history) ? cfg.history : [];
}
function _historyContext() {
const secrets = _secrets();
return { history: _currentHistory(), secrets };
}
function _deepRedact(value, secrets) {
return support.valueScrub(value, secrets || _secrets());
}
@@ -139,10 +145,9 @@ function createCollectors(deps) {
return { events: out.slice(-limit), truncated: out.length > limit };
}
function _historyErrors(history, opts) {
function _historyErrors(history, opts, secrets) {
const o = opts || {};
const sinceMs = Number.isFinite(o.sinceMs) ? o.sinceMs : null;
const secrets = _secrets();
const errors = [];
const byCategory = {};
for (const batch of (Array.isArray(history) ? history : [])) {
@@ -171,16 +176,17 @@ function createCollectors(deps) {
return { errors, byCategory };
}
function _listErrors(args, history) {
function _listErrors(args, history, secrets) {
const a = args || {};
const { errors, byCategory } = _historyErrors(history, a);
const { errors, byCategory } = _historyErrors(history, a, secrets);
const limit = Math.min(Math.max(Number(a.limit) || 100, 1), 1000);
const window = Number.isFinite(a.sinceMs) ? `since ${new Date(a.sinceMs).toISOString()}` : 'all history';
return { window, total: errors.length, byCategory, errors: errors.slice(-limit) };
}
function listErrors(args) {
return _listErrors(args, _currentHistory());
const { history, secrets } = _historyContext();
return _listErrors(args, history, secrets);
}
function getQueueState(args) {
@@ -212,12 +218,11 @@ function createCollectors(deps) {
return result;
}
function _getHistory(args, history) {
function _getHistory(args, history, secrets) {
const a = args || {};
const limit = Math.min(Math.max(Number(a.limit) || 20, 1), 200);
const perHoster = stats.summarizePerHoster(history);
const perHoster = stats.summarizePerHoster([...history]);
const recent = [...history].slice(-limit).reverse();
const secrets = _secrets();
const batches = recent.map(b => {
const out = { timestamp: b.timestamp || null, fileCount: Array.isArray(b.files) ? b.files.length : 0 };
if (a.includeFiles) {
@@ -237,7 +242,8 @@ function createCollectors(deps) {
}
function getHistory(args) {
return _getHistory(args, _currentHistory());
const { history, secrets } = _historyContext();
return _getHistory(args, history, secrets);
}
function getRotationState() {
@@ -259,10 +265,10 @@ function createCollectors(deps) {
const a = args || {};
const errorLimit = Math.min(Math.max(Number(a.errorLimit) || 20, 1), 200);
const errArgs = Number.isFinite(a.errorSinceMs) ? { sinceMs: a.errorSinceMs, limit: errorLimit } : { limit: errorLimit };
const currentHistory = _currentHistory();
const errors = _listErrors(errArgs, currentHistory);
const { history: currentHistory, secrets } = _historyContext();
const errors = _listErrors(errArgs, currentHistory, secrets);
const queue = getQueueState({ includeJobs: false });
const history = _getHistory({ limit: 5 }, currentHistory);
const history = _getHistory({ limit: 5 }, currentHistory, secrets);
const warnings = [];
if (queue.source === 'persisted' && queue.stale) warnings.push('queue state is from the persisted snapshot (may lag live state; UploadManager not introspected in this build).');
if (errors.total > 0) warnings.push(`${errors.total} non-success result(s) in the error window.`);