kin: KIN-021 Аудит-лог для --dangerously-skip-permissions в auto mode

This commit is contained in:
Gros Frumos 2026-03-16 07:13:32 +02:00
parent 67071c757d
commit a0b0976d8d
16 changed files with 1477 additions and 14 deletions

View file

@ -201,6 +201,23 @@ async function runPipeline() {
const hasSteps = computed(() => (task.value?.pipeline_steps?.length ?? 0) > 0)
const isRunning = computed(() => task.value?.status === 'in_progress')
const isManualEscalation = computed(() => task.value?.brief?.task_type === 'manual_escalation')
const resolvingManually = ref(false)
async function resolveManually() {
if (!task.value) return
if (!confirm('Пометить задачу как решённую вручную?')) return
resolvingManually.value = true
try {
const updated = await api.patchTask(props.id, { status: 'done' })
task.value = { ...task.value, ...updated }
} catch (e: any) {
error.value = e.message
} finally {
resolvingManually.value = false
}
}
function goBack() {
if (window.history.length > 1) {
@ -228,6 +245,51 @@ async function changeStatus(newStatus: string) {
statusChanging.value = false
}
}
// Edit modal (pending tasks only)
const showEdit = ref(false)
const editForm = ref({ title: '', briefText: '', priority: 5 })
const editLoading = ref(false)
const editError = ref('')
function getBriefText(brief: Record<string, unknown> | null): string {
if (!brief) return ''
if (typeof brief === 'string') return brief as string
if ('text' in brief) return String(brief.text)
return JSON.stringify(brief)
}
function openEdit() {
if (!task.value) return
editForm.value = {
title: task.value.title,
briefText: getBriefText(task.value.brief),
priority: task.value.priority,
}
editError.value = ''
showEdit.value = true
}
async function saveEdit() {
if (!task.value) return
editLoading.value = true
editError.value = ''
try {
const data: Parameters<typeof api.patchTask>[1] = {}
if (editForm.value.title !== task.value.title) data.title = editForm.value.title
if (editForm.value.priority !== task.value.priority) data.priority = editForm.value.priority
const origBriefText = getBriefText(task.value.brief)
if (editForm.value.briefText !== origBriefText) data.brief_text = editForm.value.briefText
if (Object.keys(data).length === 0) { showEdit.value = false; return }
const updated = await api.patchTask(props.id, data)
task.value = { ...task.value, ...updated }
showEdit.value = false
} catch (e: any) {
editError.value = e.message
} finally {
editLoading.value = false
}
}
</script>
<template>
@ -264,7 +326,32 @@ async function changeStatus(newStatus: string) {
<span v-if="isRunning" class="inline-block w-2 h-2 bg-blue-500 rounded-full animate-pulse"></span>
<span class="text-xs text-gray-600">pri {{ task.priority }}</span>
</div>
<div v-if="task.brief" class="text-xs text-gray-500 mb-1">
<!-- Manual escalation context banner -->
<div v-if="isManualEscalation" class="mb-3 px-3 py-2 border border-orange-800/60 bg-orange-950/20 rounded">
<div class="flex items-center gap-2 mb-1">
<span class="text-xs font-semibold text-orange-400">&#9888; Требует ручного решения</span>
<span v-if="task.parent_task_id" class="text-xs text-gray-600">
эскалация из
<router-link :to="`/task/${task.parent_task_id}`" class="text-orange-600 hover:text-orange-400">
{{ task.parent_task_id }}
</router-link>
</span>
</div>
<p class="text-xs text-orange-300">{{ task.title }}</p>
<p v-if="task.brief?.description" class="text-xs text-gray-400 mt-1">{{ task.brief.description }}</p>
<p class="text-xs text-gray-600 mt-1">Автопилот не смог выполнить это автоматически. Примите меры вручную и нажмите «Решить вручную».</p>
</div>
<!-- Dangerous skip warning banner -->
<div v-if="task.dangerously_skipped" class="mb-3 px-3 py-2 border border-red-700 bg-red-950/40 rounded flex items-start gap-2">
<span class="text-red-400 text-base shrink-0">&#9888;</span>
<div>
<span class="text-xs font-semibold text-red-400">--dangerously-skip-permissions использовался в этой задаче</span>
<p class="text-xs text-red-300/70 mt-0.5">Агент выполнял команды с обходом проверок разрешений. Проверьте pipeline-шаги и сделанные изменения.</p>
</div>
</div>
<div v-if="task.brief && !isManualEscalation" class="text-xs text-gray-500 mb-1">
Brief: {{ JSON.stringify(task.brief) }}
</div>
<div v-if="task.status === 'blocked' && task.blocked_reason" class="text-xs text-red-400 mb-1 bg-red-950/30 border border-red-800/40 rounded px-2 py-1">
@ -361,6 +448,11 @@ async function changeStatus(newStatus: string) {
:title="autoMode ? 'Auto mode: agents can write files' : 'Review mode: agents read-only'">
{{ autoMode ? '&#x1F513; Auto' : '&#x1F512; Review' }}
</button>
<button v-if="task.status === 'pending'"
@click="openEdit"
class="px-3 py-2 text-sm bg-gray-800/50 text-gray-400 border border-gray-700 rounded hover:bg-gray-800">
&#9998; Edit
</button>
<button v-if="task.status === 'pending' || task.status === 'blocked'"
@click="runPipeline"
:disabled="polling"
@ -368,6 +460,13 @@ async function changeStatus(newStatus: string) {
<span v-if="polling" class="inline-block w-3 h-3 border-2 border-blue-400 border-t-transparent rounded-full animate-spin mr-1"></span>
{{ polling ? 'Pipeline running...' : '&#9654; Run Pipeline' }}
</button>
<button v-if="isManualEscalation && task.status !== 'done' && task.status !== 'cancelled'"
@click="resolveManually"
:disabled="resolvingManually"
class="px-4 py-2 text-sm bg-orange-900/50 text-orange-400 border border-orange-800 rounded hover:bg-orange-900 disabled:opacity-50">
<span v-if="resolvingManually" class="inline-block w-3 h-3 border-2 border-orange-400 border-t-transparent rounded-full animate-spin mr-1"></span>
{{ resolvingManually ? 'Сохраняем...' : '&#10003; Решить вручную' }}
</button>
</div>
<!-- Approve Modal -->
@ -438,5 +537,31 @@ async function changeStatus(newStatus: string) {
</button>
</form>
</Modal>
<!-- Edit Modal (pending tasks only) -->
<Modal v-if="showEdit" title="Edit Task" @close="showEdit = false">
<form @submit.prevent="saveEdit" class="space-y-3">
<div>
<label class="block text-xs text-gray-500 mb-1">Title</label>
<input v-model="editForm.title" required
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600" />
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Brief</label>
<textarea v-model="editForm.briefText" rows="4" placeholder="Task description..."
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200 placeholder-gray-600 resize-y"></textarea>
</div>
<div>
<label class="block text-xs text-gray-500 mb-1">Priority (110)</label>
<input v-model.number="editForm.priority" type="number" min="1" max="10" required
class="w-full bg-gray-800 border border-gray-700 rounded px-3 py-2 text-sm text-gray-200" />
</div>
<p v-if="editError" class="text-red-400 text-xs">{{ editError }}</p>
<button type="submit" :disabled="editLoading"
class="w-full py-2 bg-blue-900/50 text-blue-400 border border-blue-800 rounded text-sm hover:bg-blue-900 disabled:opacity-50">
{{ editLoading ? 'Saving...' : 'Save' }}
</button>
</form>
</Modal>
</div>
</template>