maelys-git
Level 1 — Minimal gate
Turn normalized Git arguments into facts, solve the policy, and execute only on an explicit allow.Follow the quickstart setup for Maelys Datalog 0.11.1. Both languages use the current opaque consumer API. These are assembly fragments; argument normalization and repository discovery remain application responsibilities.
Level 1 introduces the core pattern: turn a Git command into runtime facts,
evaluate a Datalog policy, and only execute the real git binary if the
policy derives allow(Op). This level deliberately keeps the domain small:
operation, target ref, user, flags, and commit-message validity, with a small
allowlist of known operations. It is the minimal policy gate, and it is
fail-closed: anything it cannot determine with certainty is denied.
The pipeline
- git pushuser command
- EDBargv + env
- solveallow / deny
- gitor blocked
Domain
#include <maelys/datalog.h>
#include <maelys/datalog_builders.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* Minimal domain for Level 1. Level 3 extends this with
source_ref, changed_file, sensitive_file_staged,
approval_present, working_tree_dirty, branch_class. */
static const maelys_datalog_predicate_t git_preds[] = {
/* EDB: runtime command context */
{ "operation", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "target_ref", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "user", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "flag", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "commit_msg", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "valid_commit_msg", 1, MAELYS_DATALOG_PREDICATE_EDB },
/* POLICY_FACT: ruleset configuration */
{ "protected_branch", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
{ "blocked_user", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
{ "release_manager", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
{ "known_operation", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
/* IDB / QUERY: decisions.
deny/2 : query-visible for audit/debug deny-code enumeration.
deny_any/1 : internal collapse predicate used by allow/1.
allow/1 : queried by the wrapper to make the decision. */
{ "deny", 2, MAELYS_DATALOG_PREDICATE_IDB |
MAELYS_DATALOG_PREDICATE_QUERY },
{ "deny_any", 1, MAELYS_DATALOG_PREDICATE_IDB },
{ "allow", 1, MAELYS_DATALOG_PREDICATE_IDB |
MAELYS_DATALOG_PREDICATE_QUERY },
};
static const char *const POLICY_ATOMS[] = {
"main",
"master",
"release",
"ci-bot",
"alice",
"bob",
"push",
"commit",
"GIT_UNKNOWN_OPERATION",
"GIT_FORCE_PUSH",
"force",
"GIT_PROTECTED_BRANCH",
"GIT_DELETE_PROTECTED",
"delete",
"GIT_BLOCKED_USER",
"GIT_BAD_COMMIT_PREFIX"
};
static const maelys_datalog_domain_t git_domain = {
"git_policy", git_preds, sizeof(git_preds) / sizeof(git_preds[0]),
POLICY_ATOMS, sizeof(POLICY_ATOMS) / sizeof(POLICY_ATOMS[0]),
};Policy
protected_branch("main").
protected_branch("master").
protected_branch("release").
blocked_user("ci-bot").
release_manager("alice").
release_manager("bob").
known_operation("push").
known_operation("commit").
/* Unknown operation: denied by default, not allowed because no deny matched. */
deny(Op, "GIT_UNKNOWN_OPERATION") :-
operation(Op), not(known_operation(Op)).
/* Force covers --force, -f, --force-with-lease, --force-if-includes and a
leading '+' in a refspec: the C side normalizes them all to flag("force"). */
deny("push", "GIT_FORCE_PUSH") :-
operation("push"), flag("force").
deny("push", "GIT_PROTECTED_BRANCH") :-
operation("push"), target_ref(B), protected_branch(B),
user(U), not(release_manager(U)).
/* Delete covers --delete, -d, -D and a leading ':' in a refspec:
the C side normalizes them all to flag("delete"). */
deny("push", "GIT_DELETE_PROTECTED") :-
operation("push"), flag("delete"), target_ref(B), protected_branch(B).
deny("commit", "GIT_BLOCKED_USER") :-
operation("commit"), user(U), blocked_user(U).
deny("commit", "GIT_BAD_COMMIT_PREFIX") :-
operation("commit"), commit_msg(M), not(valid_commit_msg(M)).
deny_any(Op) :- deny(Op, Code).
/* allow requires known_operation(Op): an unlisted operation is denied,
never allowed merely because no deny rule happened to match it. */
allow(Op) :- operation(Op), known_operation(Op), not(deny_any(Op)).
EDB from argv
Every insertion must be checked. For example, dropping flag("force") could prevent its deny rule from firing while operation("push") still allows a decision. Dropping operation itself cannot authorize anything here: allow also requires it. C returns an error status; Python raises an exception. The caller must deny in either case.
typedef struct {
const char *operation, *user, *target_ref, *commit_msg, *source_ref;
int has_force, has_delete, needs_ref, ref_ambiguous;
} git_context_t;
static maelys_datalog_status_t populate_edb(
maelys_datalog_input_edb_t *edb, const git_context_t *ctx,
maelys_datalog_diagnostic_t *diagnostic)
{
maelys_datalog_status_t rc;
rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "operation", ctx->operation);
if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "user", ctx->user);
if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
if (ctx->target_ref) {
rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "target_ref", ctx->target_ref);
if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
}
if (ctx->has_force) {
rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "flag", "force");
if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
}
if (ctx->has_delete) {
rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "flag", "delete");
if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
}
if (ctx->commit_msg) {
rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "commit_msg", ctx->commit_msg);
if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
static const char *const prefixes[] = {
"feat:", "fix:", "chore:", "docs:", "refactor:", NULL
};
for (size_t i = 0; prefixes[i]; ++i) {
if (strncmp(ctx->commit_msg, prefixes[i], strlen(prefixes[i])) == 0) {
rc = MAELYS_DATALOG_ADD_FACT(
edb, diagnostic, "valid_commit_msg", ctx->commit_msg);
if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
break;
}
}
}
return MAELYS_DATALOG_STATUS_OK;
}Evaluate and exec
/* Exit-code convention: 0 permits Git, 1 denies it. */
static int evaluate(maelys_datalog_session_t *session, const git_context_t *ctx)
{
if (!ctx || !ctx->operation || !ctx->operation[0] || !ctx->user || !ctx->user[0])
return 1;
if (strcmp(ctx->operation, "commit") == 0 &&
(!ctx->commit_msg || !ctx->commit_msg[0])) return 1;
if (ctx->needs_ref && (ctx->ref_ambiguous || !ctx->target_ref || !ctx->target_ref[0]))
return 1;
maelys_datalog_input_edb_t *edb = NULL;
maelys_datalog_result_t *result = NULL;
maelys_datalog_diagnostic_t diagnostic = MAELYS_DATALOG_DIAGNOSTIC_INIT;
int allowed = 0;
maelys_datalog_status_t rc = maelys_datalog_input_edb_create(&edb);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
rc = populate_edb(edb, ctx, &diagnostic);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
rc = maelys_datalog_session_solve_edb(session, edb, &result, &diagnostic);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
rc = MAELYS_DATALOG_QUERY(result, &allowed, "allow", ctx->operation);
cleanup:
if (rc != MAELYS_DATALOG_STATUS_OK)
fprintf(stderr, "maelys-git: %s: %s\n",
maelys_datalog_status_name(rc), diagnostic.message);
if (result) (void)maelys_datalog_result_free(result);
(void)maelys_datalog_input_edb_free(edb);
return rc == MAELYS_DATALOG_STATUS_OK && allowed ? 0 : 1;
}Why this is fail-closed by construction
The gate checks three stages: complete context, successful evaluation, and
an explicit allow fact. Insertion validates input shape and storage bounds;
solving validates domain membership and materializes the complete request.
Every error denies, including capacity exhaustion. A successful insertion
alone does not prove that a predicate belongs to the registered domain.
There is no edb_finalize() in this API. The input buffer stays editable;
session_solve_edb() borrows it during the call. The returned result leases
the session: query it, then free it before solving the next request. Clearing
or freeing the input does not change that result. These examples allocate
opaque handles; they do not promise zero allocation.
The engine reasons over supplied facts. It cannot establish that a branch, user or staged-file snapshot is truthful. The wrapper must reject ambiguous refspecs, authenticate identity, handle acquisition failures, and protect the policy and executable. Ignoring an insertion error or asserting the wrong destination can still weaken the gate.
Load policy and run
Level 1 loads trusted policy text with maelys_datalog_policy_load_inline()
after registering the domain, then creates a session for policy index 0u.
Python registers the same vocabulary and calls load_inline_ruleset().
Level 4 uses the manifest loaders and SHA-256 verification.
/* Application callback, not an engine service. Return 0 only for complete,
supported syntax and trustworthy evidence. argv includes the wrapper name. */
typedef int (*git_context_extractor_t)(int argc, char *argv[], git_context_t *ctx);
static int run(int argc, char *argv[], git_context_extractor_t extract_context,
const char *policy_source)
{
maelys_datalog_policy_t *policy = NULL;
maelys_datalog_session_t *session = NULL;
maelys_datalog_diagnostic_t diagnostic = MAELYS_DATALOG_DIAGNOSTIC_INIT;
git_context_t ctx = {0};
int denied = 1;
maelys_datalog_status_t rc = maelys_datalog_domain_register(&git_domain);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
if (!policy_source || !extract_context || argc < 2) goto cleanup;
rc = maelys_datalog_policy_load_inline("git_policy", "git_policy.l1",
policy_source, strlen(policy_source), &policy, &diagnostic);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
rc = maelys_datalog_session_create(policy, 0u, &session);
if (rc != MAELYS_DATALOG_STATUS_OK) goto cleanup;
if (extract_context(argc, argv, &ctx) != 0) goto cleanup;
denied = evaluate(session, &ctx);
cleanup:
if (rc != MAELYS_DATALOG_STATUS_OK)
fprintf(stderr, "maelys-git startup: %s: %s\n",
maelys_datalog_status_name(rc), diagnostic.message);
(void)maelys_datalog_session_free(session);
(void)maelys_datalog_policy_free(policy);
if (denied) return 1;
/* Trusted deployment configuration supplies the absolute executable. */
argv[0] = "/usr/bin/git";
execv(argv[0], argv);
perror("maelys-git: execv");
return 1;
}Level 1 in action
$ maelys-git push origin main → BLOCKED (GIT_PROTECTED_BRANCH)
$ maelys-git push --force origin feat → BLOCKED (GIT_FORCE_PUSH)
$ maelys-git commit -m "wip" → BLOCKED (GIT_BAD_COMMIT_PREFIX)
$ maelys-git push origin → BLOCKED (ambiguous ref, preflight)
$ maelys-git status → BLOCKED (GIT_UNKNOWN_OPERATION)
$ maelys-git push origin feat/foo → ALLOWED → execv("/usr/bin/git", argv)What you have learned
Keep this model
Turn normalized Git input into an explicit decision- Normalize before inserting facts
The wrapper identifies the operation, flags and refspecs before policy evaluation. Ambiguous or incomplete command input is rejected rather than guessed.
- Check every required fact
Every fact insertion must succeed. Missing identity, operation or flag evidence could otherwise prevent a denial rule from matching.
- Execute only after an explicit allow
The wrapper runs the trusted Git binary only after successful solving and querying establish allow. Unknown operations and API errors deny.
- The wrapper supplies trustworthy context
Datalog evaluates the facts it receives; it does not authenticate the user, interpret Git arguments or prevent someone from bypassing this tutorial wrapper.