maelys-git
Level 5 — Audit records
Record decisions with denial codes, policy identity and evidence.Continue from Level 4 — Per-repository policy. Keep the declarations and helpers from the earlier levels; this page extends the same wrapper in C and Python.
Level 5 turns the wrapper into an audited gate. Every allow or deny decision produces a structured audit record containing the operation, user, target ref, policy identity, policy hash, deny code, and witness summary when available. The record — a plain JSON object the wrapper itself composes — makes the decision reviewable without relying on shell logs or re-running Git.
Audit record structure
{
"tool": "git-policy-gate",
"operation": "push",
"target_ref": "main",
"user": "carol",
"decision": "deny",
"deny_code": "GIT_PROTECTED_BRANCH",
"deny_message": "direct push to protected branch is forbidden",
"policy_id": "git_policy.main",
"policy_sha256": "a3f7c291...",
"timestamp": "2025-06-15T14:32:01Z",
"proof_summary": {
"derived": "deny(\"push\", \"GIT_PROTECTED_BRANCH\")",
"facts_used": [
"operation(\"push\")",
"target_ref(\"main\")",
"protected_branch(\"main\")"
]
}
}Emitting the audit record
static void emit_audit_record(const git_context_t *ctx,
int allowed,
const char *deny_code,
const char *policy_id,
const char *policy_sha256)
{
fprintf(stderr,
"{\n"
" \"tool\": \"git-policy-gate\",\n"
" \"operation\": \"%s\",\n"
" \"target_ref\": \"%s\",\n"
" \"user\": \"%s\",\n"
" \"decision\": \"%s\",\n"
" \"deny_code\": \"%s\",\n"
" \"deny_message\": \"%s\",\n"
" \"policy_id\": \"%s\",\n"
" \"policy_sha256\": \"%s\"\n"
"}\n",
ctx->operation,
ctx->target_ref ? ctx->target_ref : "",
ctx->user ? ctx->user : "",
allowed ? "allow" : "deny",
deny_code ? deny_code : "",
deny_code ? message_for_code(deny_code) : "",
policy_id,
policy_sha256);
}In this example, deny_code comes from enumerating the derived
deny(Op, Code) facts in the solve result. The minimal ground-query API
(MAELYS_DATALOG_QUERY / contains_fact) only proves allow/deny — it does not bind the
code value directly; enumeration recovers the exact code.
The witness summary (which rule fired, which facts were used) can be filled from the canonical Why-true document of the derived deny fact. The minimal record — decision, deny code, policy identity, and selected EDB facts — is already valuable on its own. See C querying and explanations and Python querying.
Level 5 in action
$ maelys-git push origin main
{
"tool": "maelys-git",
"operation": "push",
"target_ref": "main",
"user": "carol",
"decision": "deny",
"deny_code": "GIT_PROTECTED_BRANCH",
"deny_message": "direct push to protected branch is forbidden",
"policy_id": "git_policy.main",
"policy_sha256": "a3f7c291..."
}
maelys-git: 'push' denied by policyWhat you have learned
Keep this model
Record the decision and its evidence- Retrieve the actual denial codes
Enumerating deny facts exposes the operation and code produced by the rules. A yes-or-no membership query does not retrieve an unknown code.
- Identify what was evaluated
The wrapper builds an audit record containing the request, decision and policy identity. A witness summary can add evidence for a derived fact.
- Keep recording and execution separate
The application composes the JSON record and controls execution. The engine supplies facts and explanations, not a complete audit or process-execution service.