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

CODE
{
  "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

CODESerialize a decision 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

CODE
$ 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 policy

What 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.

Continue