From 466081aa66f53e20bfca2e93eec9a0ef7b7d6a62 Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Thu, 13 Aug 2026 15:58:29 -0400 Subject: [PATCH] Redact a bug before it is stored, and give it a report shape Evidence was kept raw, so a key in a log line reached the page and the store; and there was no format to send even once it was clean. --- Plugin/unraid/include/ai.php | 93 ++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/Plugin/unraid/include/ai.php b/Plugin/unraid/include/ai.php index 9291152..dea4354 100644 --- a/Plugin/unraid/include/ai.php +++ b/Plugin/unraid/include/ai.php @@ -1239,6 +1239,22 @@ function vv_ai_bug_write(string $component, string $summary, string $evidence, a if ($component === '' || $summary === '' || $evidence === '') return ['ok' => false]; if (!vv_ai_scope_ok($component)) return ['ok' => false]; + // Redacted on the way in, not on the way out. A report is built from this record, shown on + // the AI page from this record, and pasted into an issue from this record — redacting at any + // one of those leaves the other two holding the secret, and the store keeps it forever. + // + // Counted so the report can say something was removed. Silent redaction reads as a short log, + // and a reader who cannot tell the difference cannot judge what they are looking at. + $summary = vv_ai_redact($summary); + $evidence = vv_ai_redact($evidence); + $redacted = substr_count($summary . "\0" . $evidence, VV_AI_REDACTED); + foreach ($ctx as $k => $v) { + if (is_string($v)) { + $ctx[$k] = vv_ai_redact($v); + $redacted += substr_count($ctx[$k], VV_AI_REDACTED); + } + } + // The commit as it was when the fault was seen, not when the report is read. "Is this already // fixed" is the first question any maintainer asks, and a version captured later answers a // different question. Slot id (host1/host2) rather than the hostname — this text is written @@ -1262,6 +1278,10 @@ function vv_ai_bug_write(string $component, string $summary, string $evidence, a 'seen' => 1, 'open' => true, 'context' => array_slice($ctx, 0, 12), + 'redacted' => $redacted, + // The shape this record was written in. A report format will change; being able to read + // an old record without guessing which fields it has is what makes that survivable. + 'template' => 1, ]; $p = vv_ai_bug_file($rec); if (is_file($p)) { @@ -1277,6 +1297,79 @@ function vv_ai_bug_write(string $component, string $summary, string $evidence, a return ['ok' => true, 'id' => $rec['id'], 'seen' => $rec['seen']]; } +// Renders one stored bug as the report that gets pasted into an issue. +// +// The shape is built around one rule: observed fact and inference are separated and labelled. +// A report that blends "the log said X" with "this is probably a permissions problem" is one that +// the next reader — a person, or a model asked to look at it — will quote back as evidence. Every +// heading below is either quoted output or explicitly marked as a reading of it. +// +// Written to be pasted somewhere public, so it carries the host slot rather than the hostname and +// nothing that identifies the machine. What it does carry is the two facts a maintainer asks for +// first: which commit, and how many times. +function vv_ai_bug_report(array $b): string { + $g = fn(string $k, $d = '') => $b[$k] ?? $d; + $ctx = (array) $g('context', []); + $when = fn($t) => $t ? date('Y-m-d H:i', (int) $t) : '?'; + + $seen = (int) $g('seen', 1); + $out = "## Varaverk bug report\n\n"; + $out .= "| | |\n|---|---|\n"; + $out .= '| **Summary** | ' . str_replace('|', '\\|', (string) $g('summary')) . " |\n"; + $out .= '| **Component** | `' . $g('component') . "` |\n"; + $out .= '| **Commit** | `' . ($g('commit') ?: 'unknown') . "` |\n"; + $out .= '| **Seen** | ' . $seen . '× · first ' . $when($g('first')) + . ' · last ' . $when($g('last')) . " |\n"; + $out .= '| **Host slot** | ' . $g('host', '?') . " |\n"; + $out .= '| **Report id** | `' . $g('id', '?') . "` |\n"; + $out .= '| **Template** | ' . (int) $g('template', 1) . " |\n\n"; + + // Whether the quote below was checked against the log it claims to come from. The worker + // refuses to file a bug whose evidence it could not find, so this is normally true — but a + // report that asserts it without having checked is worth less than one that says it did not. + $verified = $ctx['verified'] ?? null; + $vLabel = $verified === true ? 'verified present in ' . ($ctx['log'] ?? 'the log') + : ($verified === false ? 'NOT found in the log — treat with suspicion' + : 'not checked against a log'); + $out .= "### Evidence — verbatim, " . $vLabel . "\n\n```\n" + . rtrim((string) $g('evidence')) . "\n```\n\n"; + + if (!empty($ctx['asked'])) { + $out .= "### What was being asked when it was noticed\n\n> " + . str_replace("\n", "\n> ", trim((string) $ctx['asked'])) . "\n\n"; + } + + $out .= "### Environment\n\n"; + $env = array_filter([ + 'Unraid ' . (vv_ai_bug_unraid_version() ?: '?'), + 'PHP ' . PHP_VERSION, + !empty($ctx['profile']) ? 'assistant profile: ' . $ctx['profile'] : null, + !empty($ctx['scope']) ? 'scope: ' . $ctx['scope'] : null, + ]); + $out .= '- ' . implode("\n- ", $env) . "\n\n"; + + // Stated even when nothing was removed, because "0 values" and no line at all read very + // differently to someone deciding whether the log looks suspiciously short. + $n = (int) $g('redacted', 0); + $out .= "### Redaction\n\n"; + $out .= $n > 0 + ? $n . ' value' . ($n === 1 ? '' : 's') . " matched a known credential or a secret-shaped " + . "assignment and " . ($n === 1 ? 'was' : 'were') . " replaced with `" . VV_AI_REDACTED . "`.\n" + : "Nothing matched a known credential. Note that redaction catches secrets held in this " + . "install's conf, or written as an assignment — an unlabelled token printed by another " + . "program would not be caught, so read the evidence before sending.\n"; + + return $out; +} + +// Best effort, and blank rather than wrong. /etc/unraid-version is a shell assignment; on +// anything that is not Unraid there is simply no file and the report says "?". +function vv_ai_bug_unraid_version(): string { + $raw = @file_get_contents('/etc/unraid-version'); + if (!is_string($raw)) return ''; + return preg_match('/"([^"]+)"/', $raw, $m) ? $m[1] : trim(explode('=', $raw, 2)[1] ?? ''); +} + function vv_ai_bugs_list(bool $openOnly = true): array { $out = []; foreach ((array)@glob(vv_ai_bugs_dir() . '/*.json') as $f) {