Skip to content

Commit f8ac6c3

Browse files
hyperpolymathclaude
andcommitted
feat(vault-mcp): add audit log, command allowlist, and MCP tool defs
- Zig FFI: audit ring buffer (128 entries), command allowlist with prefix matching, enforcement toggle. All thread-safe via mutex. - V adapter: /vault/audit and /vault/audit/raw endpoints for PanLL panel log-stream widget. Allowlist add/enforce/status endpoints. - MCP tools: 5 tool definitions (execute, list, status, verify, audit) for BoJ's MCP SSE transport surface. - Idris2 ABI: AuditEntry record, AllowlistEntry type, C-ABI exports. - Tests: audit ring buffer, allowlist enforcement, blocked commands. The zero-knowledge invariant is preserved: audit entries record hints and action types only — never actual credential values. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent a2f8869 commit f8ac6c3

5 files changed

Lines changed: 475 additions & 4 deletions

File tree

cartridges/vault-mcp/abi/VaultMcp/SafeSecrets.idr

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,47 @@ toolRequiresUnlock ToolVaultVerify = True
233233
export
234234
toolCount : Nat
235235
toolCount = 5
236+
237+
-- ---------------------------------------------------------------------------
238+
-- Audit log types
239+
-- ---------------------------------------------------------------------------
240+
241+
||| An audit log entry records a vault operation with its outcome.
242+
||| The audit ring buffer retains the most recent MAX_AUDIT_ENTRIES entries.
243+
||| Credential values are NEVER recorded — only hints and action types.
244+
public export
245+
record AuditEntry where
246+
constructor MkAuditEntry
247+
timestamp : Int
248+
action : VaultAction
249+
credentialHint : CredentialHint
250+
resultCode : Int
251+
agentId : String
252+
253+
-- ---------------------------------------------------------------------------
254+
-- Command allowlist
255+
-- ---------------------------------------------------------------------------
256+
257+
||| A command prefix pattern in the AI agent allowlist.
258+
||| When enforcement is enabled, vault/execute rejects commands not matching
259+
||| any registered prefix. This prevents AI agents from running arbitrary
260+
||| commands with vault-injected credentials.
261+
public export
262+
data AllowlistEntry = MkAllowlistEntry String
263+
264+
||| Extract the pattern string for FFI serialisation.
265+
export
266+
allowlistPattern : AllowlistEntry -> String
267+
allowlistPattern (MkAllowlistEntry s) = s
268+
269+
||| C-ABI export for audit entry count query.
270+
export
271+
vault_mcp_audit_count : Int
272+
273+
||| C-ABI export for allowlist add.
274+
export
275+
vault_mcp_allowlist_add : String -> Int -> Int
276+
277+
||| C-ABI export for allowlist enforcement toggle.
278+
export
279+
vault_mcp_allowlist_enforce : Int -> ()

cartridges/vault-mcp/adapter/vault_mcp_adapter.v

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ fn C.vault_mcp_rotate(hint_ptr &u8, hint_len int) int
2828
fn C.vault_mcp_read_result(out_ptr &u8, max_len int) int
2929
fn C.vault_mcp_read_error(out_ptr &u8, max_len int) int
3030
fn C.vault_mcp_reset()
31+
fn C.vault_mcp_audit_count() int
32+
fn C.vault_mcp_audit_entry(index int) int
33+
fn C.vault_mcp_allowlist_add(pattern_ptr &u8, pattern_len int) int
34+
fn C.vault_mcp_allowlist_enforce(enabled int)
35+
fn C.vault_mcp_allowlist_status() int
36+
fn C.vault_mcp_allowlist_count() int
3137

3238
// ---------------------------------------------------------------------------
3339
// Type definitions (matching Zig/Idris2 exactly)
@@ -245,3 +251,103 @@ pub fn can_transition(from int, to int) TransitionResponse {
245251
pub fn reset() {
246252
C.vault_mcp_reset()
247253
}
254+
255+
// ---------------------------------------------------------------------------
256+
// Audit log
257+
// ---------------------------------------------------------------------------
258+
259+
/// Audit log entry returned from the ring buffer.
260+
struct AuditEntry {
261+
timestamp i64
262+
action string
263+
credential_hint string
264+
result string
265+
agent string
266+
}
267+
268+
/// GET /vault/audit — query recent vault operations from the audit ring buffer.
269+
/// Returns up to `max_entries` most recent entries (0 = most recent first).
270+
pub fn vault_audit(max_entries int) []AuditEntry {
271+
count := C.vault_mcp_audit_count()
272+
limit := if max_entries > 0 && max_entries < count { max_entries } else { count }
273+
274+
mut entries := []AuditEntry{cap: limit}
275+
for i in 0 .. limit {
276+
n := C.vault_mcp_audit_entry(i)
277+
if n > 0 {
278+
raw := read_result()
279+
// Parse the JSON line from the result buffer
280+
// The FFI returns JSON like: {"timestamp":..., "action":..., ...}
281+
entries << AuditEntry{
282+
timestamp: 0
283+
action: ''
284+
credential_hint: ''
285+
result: ''
286+
agent: ''
287+
}
288+
// For now, store raw JSON; structured parse deferred to V json.decode
289+
if entries.len > 0 {
290+
entries[entries.len - 1] = AuditEntry{
291+
timestamp: 0
292+
action: raw
293+
credential_hint: ''
294+
result: ''
295+
agent: ''
296+
}
297+
}
298+
}
299+
}
300+
return entries
301+
}
302+
303+
/// GET /vault/audit/raw — return audit entries as raw JSON lines.
304+
/// This is the preferred endpoint for the PanLL panel log-stream widget.
305+
pub fn vault_audit_raw(max_entries int) string {
306+
count := C.vault_mcp_audit_count()
307+
limit := if max_entries > 0 && max_entries < count { max_entries } else { count }
308+
309+
mut lines := []string{cap: limit}
310+
for i in 0 .. limit {
311+
n := C.vault_mcp_audit_entry(i)
312+
if n > 0 {
313+
lines << read_result()
314+
}
315+
}
316+
return lines.join('\n')
317+
}
318+
319+
// ---------------------------------------------------------------------------
320+
// Command allowlist (AI agent safety)
321+
// ---------------------------------------------------------------------------
322+
323+
/// POST /vault/allowlist/add — add a command prefix to the AI agent allowlist.
324+
/// Commands matching this prefix will be permitted by vault/execute.
325+
/// Example: "git push" allows "git push origin main" etc.
326+
pub fn vault_allowlist_add(pattern string) !string {
327+
result := C.vault_mcp_allowlist_add(pattern.str, pattern.len)
328+
return match result {
329+
0 { 'added: ${pattern}' }
330+
-1 { return error('allowlist is full') }
331+
-3 { return error('invalid pattern') }
332+
else { return error('unknown error (code ${result})') }
333+
}
334+
}
335+
336+
/// POST /vault/allowlist/enforce — enable or disable allowlist enforcement.
337+
/// When enabled, vault/execute rejects commands not matching any allowlist prefix.
338+
pub fn vault_allowlist_enforce(enabled bool) {
339+
C.vault_mcp_allowlist_enforce(if enabled { 1 } else { 0 })
340+
}
341+
342+
/// GET /vault/allowlist/status — query allowlist enforcement state.
343+
struct AllowlistStatus {
344+
enforced bool
345+
count int
346+
}
347+
348+
pub fn vault_allowlist_status() AllowlistStatus {
349+
return AllowlistStatus{
350+
enforced: C.vault_mcp_allowlist_status() == 1
351+
count: C.vault_mcp_allowlist_count()
352+
}
353+
}

0 commit comments

Comments
 (0)