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.
This commit is contained in:
Gmer4Lfe
2026-08-13 15:58:29 -04:00
parent be6bb3ffc5
commit 466081aa66
+93
View File
@@ -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) {