arkaos 3.33.0 → 3.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.
|
|
1
|
+
3.34.0
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// PR90d v3.34.0 — Audit log page.
|
|
3
|
+
//
|
|
4
|
+
// Lists hook bypass / block events from the enforcement telemetry log.
|
|
5
|
+
// Operators filter by kind (bypass / blocked) and tool name.
|
|
6
|
+
|
|
7
|
+
interface AuditEvent {
|
|
8
|
+
ts: string
|
|
9
|
+
tool: string
|
|
10
|
+
reason: string
|
|
11
|
+
cwd: string
|
|
12
|
+
bypass_used: boolean
|
|
13
|
+
kind: 'bypass' | 'blocked'
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const { fetchApi } = useApi()
|
|
17
|
+
|
|
18
|
+
const kind = ref<'all' | 'bypass' | 'blocked'>('all')
|
|
19
|
+
const toolFilter = ref('')
|
|
20
|
+
const limit = ref(100)
|
|
21
|
+
|
|
22
|
+
const { data, status, error, refresh } = await fetchApi<{
|
|
23
|
+
events: AuditEvent[]
|
|
24
|
+
total: number
|
|
25
|
+
}>(
|
|
26
|
+
'/api/audit',
|
|
27
|
+
{
|
|
28
|
+
query: computed(() => ({
|
|
29
|
+
limit: limit.value,
|
|
30
|
+
kind: kind.value === 'all' ? undefined : kind.value,
|
|
31
|
+
tool: toolFilter.value.trim() || undefined,
|
|
32
|
+
})),
|
|
33
|
+
},
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const events = computed<AuditEvent[]>(() => data.value?.events ?? [])
|
|
37
|
+
|
|
38
|
+
const kindOptions = [
|
|
39
|
+
{ label: 'All kinds', value: 'all' },
|
|
40
|
+
{ label: 'Bypass used', value: 'bypass' },
|
|
41
|
+
{ label: 'Blocked', value: 'blocked' },
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
function formatTs(ts: string): string {
|
|
45
|
+
if (!ts) return '—'
|
|
46
|
+
try {
|
|
47
|
+
return new Date(ts).toLocaleString()
|
|
48
|
+
} catch {
|
|
49
|
+
return ts
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function kindColor(k: string): 'warning' | 'error' | 'neutral' {
|
|
54
|
+
return k === 'bypass' ? 'warning' : k === 'blocked' ? 'error' : 'neutral'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
watch([kind, toolFilter, limit], async () => {
|
|
58
|
+
await refresh()
|
|
59
|
+
})
|
|
60
|
+
</script>
|
|
61
|
+
|
|
62
|
+
<template>
|
|
63
|
+
<UDashboardPanel id="audit">
|
|
64
|
+
<template #header>
|
|
65
|
+
<UDashboardNavbar title="Audit log">
|
|
66
|
+
<template #leading>
|
|
67
|
+
<UDashboardSidebarCollapse />
|
|
68
|
+
</template>
|
|
69
|
+
<template #trailing>
|
|
70
|
+
<UBadge v-if="data?.total" :label="`${data.total} event${data.total === 1 ? '' : 's'}`" variant="subtle" />
|
|
71
|
+
</template>
|
|
72
|
+
</UDashboardNavbar>
|
|
73
|
+
</template>
|
|
74
|
+
|
|
75
|
+
<template #body>
|
|
76
|
+
<DashboardState
|
|
77
|
+
:status="status"
|
|
78
|
+
:error="error"
|
|
79
|
+
:empty="!events.length"
|
|
80
|
+
empty-title="No audit events"
|
|
81
|
+
empty-description="Hook bypass + block events show up here. Nothing yet — good news."
|
|
82
|
+
empty-icon="i-lucide-shield-check"
|
|
83
|
+
loading-label="Loading audit"
|
|
84
|
+
:on-retry="() => refresh()"
|
|
85
|
+
>
|
|
86
|
+
<div class="flex flex-wrap items-center gap-3 mb-4">
|
|
87
|
+
<USelect
|
|
88
|
+
v-model="kind"
|
|
89
|
+
:items="kindOptions"
|
|
90
|
+
placeholder="Kind"
|
|
91
|
+
class="min-w-40"
|
|
92
|
+
aria-label="Filter by kind"
|
|
93
|
+
/>
|
|
94
|
+
<UInput
|
|
95
|
+
v-model="toolFilter"
|
|
96
|
+
class="max-w-xs"
|
|
97
|
+
icon="i-lucide-wrench"
|
|
98
|
+
placeholder="Filter by tool…"
|
|
99
|
+
/>
|
|
100
|
+
<span class="ml-auto text-xs text-muted">
|
|
101
|
+
{{ events.length }} match{{ events.length === 1 ? '' : 'es' }}
|
|
102
|
+
</span>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<ul class="space-y-2 max-w-5xl">
|
|
106
|
+
<li
|
|
107
|
+
v-for="(ev, idx) in events"
|
|
108
|
+
:key="`${ev.ts}-${idx}`"
|
|
109
|
+
class="rounded-lg border border-default p-3"
|
|
110
|
+
>
|
|
111
|
+
<div class="flex items-center gap-3 flex-wrap text-xs mb-1.5">
|
|
112
|
+
<UBadge :label="ev.kind" :color="kindColor(ev.kind)" variant="subtle" size="sm" />
|
|
113
|
+
<UBadge :label="ev.tool || '—'" variant="outline" size="sm" />
|
|
114
|
+
<span class="text-muted font-mono">{{ formatTs(ev.ts) }}</span>
|
|
115
|
+
</div>
|
|
116
|
+
<p class="text-sm">{{ ev.reason || '(no reason recorded)' }}</p>
|
|
117
|
+
<p v-if="ev.cwd" class="text-xs text-muted font-mono mt-1 truncate" :title="ev.cwd">
|
|
118
|
+
{{ ev.cwd }}
|
|
119
|
+
</p>
|
|
120
|
+
</li>
|
|
121
|
+
</ul>
|
|
122
|
+
</DashboardState>
|
|
123
|
+
</template>
|
|
124
|
+
</UDashboardPanel>
|
|
125
|
+
</template>
|
package/package.json
CHANGED
package/pyproject.toml
CHANGED
|
Binary file
|
package/scripts/dashboard-api.py
CHANGED
|
@@ -2251,6 +2251,58 @@ def _last_commit_days(project_path: str) -> Optional[int]:
|
|
|
2251
2251
|
return None
|
|
2252
2252
|
|
|
2253
2253
|
|
|
2254
|
+
@app.get("/api/audit")
|
|
2255
|
+
def audit_log(
|
|
2256
|
+
limit: int = 100,
|
|
2257
|
+
kind: Optional[str] = None,
|
|
2258
|
+
tool: Optional[str] = None,
|
|
2259
|
+
):
|
|
2260
|
+
"""PR90d v3.34.0 — paginated audit log of bypass/block events.
|
|
2261
|
+
|
|
2262
|
+
Reads the same telemetry file as `_recent_incidents` but returns
|
|
2263
|
+
a larger window and accepts filter params. ``kind`` is one of
|
|
2264
|
+
``"bypass"`` / ``"blocked"``. ``tool`` filters by tool name
|
|
2265
|
+
(Edit/Write/Bash/…).
|
|
2266
|
+
"""
|
|
2267
|
+
log = Path.home() / ".arkaos" / "telemetry" / "enforcement.jsonl"
|
|
2268
|
+
if not log.exists():
|
|
2269
|
+
return {"events": [], "total": 0}
|
|
2270
|
+
try:
|
|
2271
|
+
text = log.read_text(encoding="utf-8", errors="replace")
|
|
2272
|
+
except OSError:
|
|
2273
|
+
return {"events": [], "total": 0}
|
|
2274
|
+
events: list[dict] = []
|
|
2275
|
+
cap = max(0, min(int(limit), 500))
|
|
2276
|
+
for line in reversed(text.splitlines()):
|
|
2277
|
+
if len(events) >= cap:
|
|
2278
|
+
break
|
|
2279
|
+
if not line.strip():
|
|
2280
|
+
continue
|
|
2281
|
+
try:
|
|
2282
|
+
entry = json.loads(line)
|
|
2283
|
+
except json.JSONDecodeError:
|
|
2284
|
+
continue
|
|
2285
|
+
bypass = bool(entry.get("bypass_used"))
|
|
2286
|
+
allowed = entry.get("allow")
|
|
2287
|
+
if not bypass and allowed is not False:
|
|
2288
|
+
continue
|
|
2289
|
+
row_kind = "bypass" if bypass else "blocked"
|
|
2290
|
+
if kind and kind != row_kind:
|
|
2291
|
+
continue
|
|
2292
|
+
row_tool = entry.get("tool", "")
|
|
2293
|
+
if tool and tool != row_tool:
|
|
2294
|
+
continue
|
|
2295
|
+
events.append({
|
|
2296
|
+
"ts": entry.get("ts", ""),
|
|
2297
|
+
"tool": row_tool,
|
|
2298
|
+
"reason": entry.get("reason", ""),
|
|
2299
|
+
"cwd": entry.get("cwd", ""),
|
|
2300
|
+
"bypass_used": bypass,
|
|
2301
|
+
"kind": row_kind,
|
|
2302
|
+
})
|
|
2303
|
+
return {"events": events, "total": len(events)}
|
|
2304
|
+
|
|
2305
|
+
|
|
2254
2306
|
def _recent_incidents(limit: int = 8) -> list[dict]:
|
|
2255
2307
|
"""Recent enforcement / bypass events from telemetry.
|
|
2256
2308
|
|