Flag destructive operations in generated scripts
Custom Scripts run as root on a schedule, so the expensive mistake is not tangled logic — it is a simple script aimed one directory too high. The prompt now asks for the dry-run form and an explicit line on what gets destroyed, and a scan of the generated code blocks raises a banner regardless of whether the model bothered to warn. Scans fenced code only, so prose mentioning rm does not trip it.
This commit is contained in:
@@ -204,6 +204,15 @@ if ($profile === 'chat') {
|
|||||||
. "they run at 3am. If you are unsure a flag exists, say so rather than picking the "
|
. "they run at 3am. If you are unsure a flag exists, say so rather than picking the "
|
||||||
. "one that sounds right.\n\n"
|
. "one that sounds right.\n\n"
|
||||||
. "Treat this as a first draft to be tested, and say so.\n\n"
|
. "Treat this as a first draft to be tested, and say so.\n\n"
|
||||||
|
. "These scripts run as root, on a schedule, unattended. So the danger is not "
|
||||||
|
. "complicated logic — it is a simple script aimed at the wrong path. If your script "
|
||||||
|
. "deletes, moves, overwrites or truncates anything (rm, mv, find -delete, "
|
||||||
|
. "rsync --delete, truncation with >, chown -R, chmod -R), then before the script "
|
||||||
|
. "say in one line exactly what it will destroy and under what conditions. Where the "
|
||||||
|
. "tool supports it, give the dry-run form first (rsync --dry-run, find without "
|
||||||
|
. "-delete) and tell them to run that and read the output before the real one. "
|
||||||
|
. "Guard every destructive path against an unset or empty variable — \"rm -rf "
|
||||||
|
. "\$DEST/\" with DEST unset is the mistake that actually happens.\n\n"
|
||||||
. "If the operator comes back with an error showing that something you suggested does "
|
. "If the operator comes back with an error showing that something you suggested does "
|
||||||
. "not exist — an unknown option, a command not found — say plainly that you got it "
|
. "not exist — an unknown option, a command not found — say plainly that you got it "
|
||||||
. "wrong and invented it. Do NOT explain it away as a version difference, a "
|
. "wrong and invented it. Do NOT explain it away as a version difference, a "
|
||||||
@@ -320,6 +329,31 @@ if ($answer === '') {
|
|||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Destructive-operation scan of what was actually generated. The prompt asks the model to warn;
|
||||||
|
// this does not depend on it having done so. These scripts run as root on a schedule, so the
|
||||||
|
// expensive mistake is not tangled logic — it is a simple script pointed one directory too high.
|
||||||
|
// Scans only fenced code, so prose mentioning "rm" does not trip it.
|
||||||
|
$warnings = [];
|
||||||
|
if ($profile === 'code' && preg_match_all('/```(?:\w+)?\n(.*?)```/s', $answer, $blocks)) {
|
||||||
|
$code = implode("\n", $blocks[1]);
|
||||||
|
$checks = [
|
||||||
|
'/(^|[;&|\s])rm\s+(-[a-zA-Z]*\s+)*/m' => 'deletes files (rm)',
|
||||||
|
'/(^|[;&|\s])mv\s/m' => 'moves files (mv)',
|
||||||
|
'/(?<!-)-delete\b/' => 'deletes files (find -delete)',
|
||||||
|
'/--delete\b/' => 'deletes at the destination (rsync --delete)',
|
||||||
|
'/(^|[;&|\s])(shred|truncate)\s/m' => 'destroys file contents',
|
||||||
|
'/(^|[;&|\s])(dd|mkfs\.\w+|mkfs)\s/m' => 'writes raw to a device',
|
||||||
|
'/(^|[;&|\s])ch(own|mod)\s+-[a-zA-Z]*R/m' => 'recursively changes ownership or permissions',
|
||||||
|
];
|
||||||
|
foreach ($checks as $re => $label) {
|
||||||
|
if (preg_match($re, $code)) $warnings[] = $label;
|
||||||
|
}
|
||||||
|
// An unquoted or unguarded path variable is what turns any of the above into a disaster.
|
||||||
|
if ($warnings && preg_match('/(rm|mv|rsync)[^\n]*\$\{?[A-Za-z_][A-Za-z0-9_]*\}?(?![\w"])/', $code)) {
|
||||||
|
$warnings[] = 'uses a path variable in a destructive command — confirm it can never be empty';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$evalCount = (int)($d['eval_count'] ?? 0);
|
$evalCount = (int)($d['eval_count'] ?? 0);
|
||||||
$evalNs = (int)($d['eval_duration'] ?? 0);
|
$evalNs = (int)($d['eval_duration'] ?? 0);
|
||||||
|
|
||||||
@@ -328,6 +362,7 @@ jw($jobFile, [
|
|||||||
'answer' => $answer,
|
'answer' => $answer,
|
||||||
'thinking' => $thinking,
|
'thinking' => $thinking,
|
||||||
'sources' => $sources,
|
'sources' => $sources,
|
||||||
|
'warnings' => array_values(array_unique($warnings)),
|
||||||
'timing' => [
|
'timing' => [
|
||||||
'retrieve_ms' => (int)round($tRetrieve * 1000),
|
'retrieve_ms' => (int)round($tRetrieve * 1000),
|
||||||
'generate_ms' => (int)round((microtime(true) - $t1) * 1000),
|
'generate_ms' => (int)round((microtime(true) - $t1) * 1000),
|
||||||
|
|||||||
@@ -110,6 +110,11 @@ if (is_dir('/var/log/varaverk')) {
|
|||||||
.vv-ai-cite { color:#5c7cfa; font-weight:bold; cursor:pointer; }
|
.vv-ai-cite { color:#5c7cfa; font-weight:bold; cursor:pointer; }
|
||||||
.vv-ai-cite:hover { text-decoration:underline; }
|
.vv-ai-cite:hover { text-decoration:underline; }
|
||||||
|
|
||||||
|
.vv-ai-danger { background:#1f0d0d; border:1px solid #4a1f1f; border-left:3px solid #e57;
|
||||||
|
border-radius:4px; padding:8px 10px; margin-bottom:9px; font-size:11px;
|
||||||
|
line-height:1.55; color:#d99; }
|
||||||
|
.vv-ai-danger strong { color:#f88; }
|
||||||
|
|
||||||
.vv-ai-think-t { font-size:10px; color:#4a4a4a; cursor:pointer; user-select:none; margin-bottom:6px;
|
.vv-ai-think-t { font-size:10px; color:#4a4a4a; cursor:pointer; user-select:none; margin-bottom:6px;
|
||||||
display:inline-block; border:1px solid #222; border-radius:3px; padding:2px 7px; }
|
display:inline-block; border:1px solid #222; border-radius:3px; padding:2px 7px; }
|
||||||
.vv-ai-think-t:hover { color:#777; border-color:#333; }
|
.vv-ai-think-t:hover { color:#777; border-color:#333; }
|
||||||
@@ -440,6 +445,13 @@ if (is_dir('/var/log/varaverk')) {
|
|||||||
+ `reasoning (${job.thinking.length.toLocaleString()} chars)</div>`
|
+ `reasoning (${job.thinking.length.toLocaleString()} chars)</div>`
|
||||||
+ `<div class="vv-ai-think">${esc(job.thinking)}</div>`;
|
+ `<div class="vv-ai-think">${esc(job.thinking)}</div>`;
|
||||||
}
|
}
|
||||||
|
// Detected from the generated code, not from the model saying so. These scripts run as root
|
||||||
|
// on a schedule, so the banner is louder than the prose warning it may or may not have added.
|
||||||
|
if (job.warnings && job.warnings.length) {
|
||||||
|
h += `<div class="vv-ai-danger"><strong>Destructive — read before running.</strong> `
|
||||||
|
+ `This script ${job.warnings.map(esc).join('; ')}. `
|
||||||
|
+ `Run any dry-run form first and check the paths are what you expect.</div>`;
|
||||||
|
}
|
||||||
h += `<div class="vv-ai-body">${fmt(job.answer)}</div>`;
|
h += `<div class="vv-ai-body">${fmt(job.answer)}</div>`;
|
||||||
h += sourcesHtml(job.sources);
|
h += sourcesHtml(job.sources);
|
||||||
const t = job.timing || {};
|
const t = job.timing || {};
|
||||||
|
|||||||
Reference in New Issue
Block a user