mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(ui): surface the auto-approve outcome on audit rows with a create-grant shortcut
This commit is contained in:
parent
aa54058e31
commit
be7b236cb5
@ -1861,6 +1861,16 @@ export default {
|
||||
},
|
||||
noLogs: 'No audit logs yet',
|
||||
expandFindings: 'Expand Details',
|
||||
outcome: {
|
||||
AUTO_GRANT: 'Auto-approved',
|
||||
HARD_BLOCK: 'Blocked by safety floor',
|
||||
FORCE_HUMAN: 'Dangerous pattern, human approval forced',
|
||||
SEVERITY_CRITICAL: 'Not auto-approved: CRITICAL always requires a human',
|
||||
SEVERITY_CEILING: 'Not auto-approved: grant ceiling below this finding ({ceiling} < {actual})',
|
||||
UNKNOWN_WORKSPACE: 'Not auto-approved: conversation workspace unknown',
|
||||
NO_GRANT: 'Not auto-approved: no matching grant',
|
||||
createGrant: 'Create a grant',
|
||||
},
|
||||
},
|
||||
severity: {
|
||||
CRITICAL: 'Critical',
|
||||
|
||||
@ -1735,6 +1735,16 @@ export default {
|
||||
},
|
||||
noLogs: '暂无审计记录',
|
||||
expandFindings: '展开详情',
|
||||
outcome: {
|
||||
AUTO_GRANT: '已自动放行',
|
||||
HARD_BLOCK: '安全地板阻断',
|
||||
FORCE_HUMAN: '高危模式,强制人审',
|
||||
SEVERITY_CRITICAL: '未自动放行:CRITICAL 永远人审',
|
||||
SEVERITY_CEILING: '未自动放行:策略严重度上限低于本次命中({ceiling} < {actual})',
|
||||
UNKNOWN_WORKSPACE: '未自动放行:无法确定会话所属工作区',
|
||||
NO_GRANT: '未自动放行:无匹配的自动批准策略',
|
||||
createGrant: '去创建策略',
|
||||
},
|
||||
},
|
||||
severity: {
|
||||
CRITICAL: '严重',
|
||||
|
||||
@ -110,6 +110,17 @@
|
||||
<span class="decision-badge" :class="'decision-' + log.decision?.toLowerCase()">
|
||||
{{ t('security.decision.' + log.decision) || log.decision }}
|
||||
</span>
|
||||
<!-- Auto-approve resolution outcome: why this NEEDS_APPROVAL call
|
||||
was (not) auto-approved. NULL on rows predating the column
|
||||
or on decisions that never reach the auto-grant layer. -->
|
||||
<div
|
||||
v-if="log.autoApproveOutcome"
|
||||
class="outcome-chip"
|
||||
:class="outcomeClass(log.autoApproveOutcome)"
|
||||
:title="log.autoApproveOutcome"
|
||||
>
|
||||
{{ outcomeText(log.autoApproveOutcome) }}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="log.maxSeverity" class="severity-badge" :class="'severity-' + log.maxSeverity?.toLowerCase()">
|
||||
@ -119,7 +130,7 @@
|
||||
<td class="cell-conv">{{ truncateConvId(log.conversationId) }}</td>
|
||||
<td>
|
||||
<button
|
||||
v-if="log.findingsJson"
|
||||
v-if="log.findingsJson || log.autoApproveOutcome"
|
||||
class="action-btn"
|
||||
@click="toggleExpand(log.id)"
|
||||
:title="t('security.audit.expandFindings')"
|
||||
@ -145,6 +156,14 @@
|
||||
<span class="finding-title">{{ finding.title }}</span>
|
||||
<span v-if="finding.remediation" class="finding-remediation">{{ finding.remediation }}</span>
|
||||
</div>
|
||||
<!-- Fixable misses (no grant, or a grant with a too-low ceiling)
|
||||
get a one-click jump to the grant form, prefilled with this
|
||||
row's tool and actual severity. -->
|
||||
<div v-if="canCreateGrantFrom(log)" class="outcome-actions">
|
||||
<button class="btn-secondary btn-sm" @click="goCreateGrant(log)">
|
||||
{{ t('security.audit.outcome.createGrant') }} →
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@ -174,11 +193,13 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { securityApi } from '@/api'
|
||||
import { parseFindings, formatTime, truncateConvId } from '../composables/helpers'
|
||||
import type { AuditStats } from '@/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
|
||||
const auditLogs = ref<any[]>([])
|
||||
const auditStats = reactive<AuditStats>({ total: 0, blocked: 0, needsApproval: 0, allowed: 0 })
|
||||
@ -244,6 +265,55 @@ async function loadAuditStats() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits an auto_approve_outcome code into an i18n key plus params.
|
||||
* SEVERITY_CEILING carries detail as "SEVERITY_CEILING:LOW<HIGH";
|
||||
* FORCE_HUMAN carries the floor pattern as "FORCE_HUMAN:pattern".
|
||||
*/
|
||||
function parseOutcome(oc: string): { key: string; params: Record<string, string> } {
|
||||
if (oc.startsWith('SEVERITY_CEILING:')) {
|
||||
const [ceiling, actual] = oc.slice('SEVERITY_CEILING:'.length).split('<')
|
||||
return { key: 'SEVERITY_CEILING', params: { ceiling: ceiling || '?', actual: actual || '?' } }
|
||||
}
|
||||
if (oc.startsWith('FORCE_HUMAN')) {
|
||||
return { key: 'FORCE_HUMAN', params: {} }
|
||||
}
|
||||
return { key: oc, params: {} }
|
||||
}
|
||||
|
||||
function outcomeText(oc: string): string {
|
||||
const { key, params } = parseOutcome(oc)
|
||||
const i18nKey = `security.audit.outcome.${key}`
|
||||
const txt = t(i18nKey, params)
|
||||
// vue-i18n echoes the key back when no message exists — fall back to the raw code.
|
||||
return txt === i18nKey ? oc : txt
|
||||
}
|
||||
|
||||
function outcomeClass(oc: string): string {
|
||||
const { key } = parseOutcome(oc)
|
||||
if (key === 'AUTO_GRANT') return 'outcome-granted'
|
||||
if (key === 'HARD_BLOCK') return 'outcome-blocked'
|
||||
return 'outcome-denied'
|
||||
}
|
||||
|
||||
/** Only misses fixable by creating a grant get the shortcut. */
|
||||
function canCreateGrantFrom(log: any): boolean {
|
||||
if (!log.autoApproveOutcome) return false
|
||||
const { key } = parseOutcome(log.autoApproveOutcome)
|
||||
return key === 'SEVERITY_CEILING' || key === 'NO_GRANT'
|
||||
}
|
||||
|
||||
function goCreateGrant(log: any) {
|
||||
router.push({
|
||||
name: 'SecurityAutoApprove',
|
||||
query: {
|
||||
create: '1',
|
||||
tool: log.toolName || '',
|
||||
severity: log.maxSeverity || '',
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function toggleExpand(id: number) {
|
||||
if (expandedRows.value.has(id)) {
|
||||
expandedRows.value.delete(id)
|
||||
@ -438,6 +508,25 @@ onMounted(async () => {
|
||||
.finding-title { color: var(--mc-text-primary); }
|
||||
.finding-remediation { color: var(--mc-text-tertiary); font-style: italic; }
|
||||
|
||||
/* Auto-approve outcome chip — sits under the decision badge. Three tones:
|
||||
granted (auto-approved), denied (fell back to human), blocked (safety floor). */
|
||||
.outcome-chip {
|
||||
display: inline-block;
|
||||
margin-top: 4px;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.outcome-granted { background: rgba(16, 185, 129, 0.12); color: #10b981; }
|
||||
.outcome-denied { background: rgba(245, 158, 11, 0.12); color: #b45309; }
|
||||
.outcome-blocked { background: rgba(239, 68, 68, 0.12); color: #ef4444; }
|
||||
|
||||
.outcome-actions {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -341,6 +341,7 @@
|
||||
import { ElMessage } from 'element-plus/es/components/message/index'
|
||||
import { ref, computed, onMounted, reactive, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import {
|
||||
Delete,
|
||||
Lock,
|
||||
@ -364,6 +365,8 @@ import type {
|
||||
} from '@/types'
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
const agentStore = useAgentStore()
|
||||
@ -603,7 +606,34 @@ function formatDate(s: string | null): string {
|
||||
return d.toLocaleString()
|
||||
}
|
||||
|
||||
onMounted(loadGrants)
|
||||
onMounted(() => {
|
||||
loadGrants()
|
||||
applyCreateQuery()
|
||||
})
|
||||
|
||||
/**
|
||||
* Entry from the audit-log page's "create grant" shortcut:
|
||||
* /security/auto-approve?create=1&tool=<name>&severity=<LOW|MEDIUM|HIGH>
|
||||
* opens the create dialog prefilled with the missed invocation's tool and
|
||||
* actual severity (WORKSPACE scope, current workspace). The query is
|
||||
* consumed once and stripped so refresh/back doesn't re-open the dialog.
|
||||
*/
|
||||
function applyCreateQuery() {
|
||||
if (route.query.create !== '1') return
|
||||
const tool = typeof route.query.tool === 'string' ? route.query.tool : ''
|
||||
const severity = typeof route.query.severity === 'string' ? route.query.severity : ''
|
||||
openCreateDialog(false)
|
||||
form.scopeType = 'WORKSPACE'
|
||||
prefillScopeId()
|
||||
form.toolName = tool
|
||||
if (severity === 'LOW' || severity === 'MEDIUM' || severity === 'HIGH') {
|
||||
form.maxSeverity = severity
|
||||
} else if (severity === 'CRITICAL') {
|
||||
// CRITICAL is never auto-approvable; HIGH is the closest configurable ceiling.
|
||||
form.maxSeverity = 'HIGH'
|
||||
}
|
||||
router.replace({ query: {} })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user