maelys-git
Level 3 — Rich Git context
Add files, branch classes, repository state and trustworthy identity.Continue from Level 2 — Structured deny codes. Keep the declarations and helpers from the earlier levels; this page extends the same wrapper in C and Python.
Level 3 enriches the EDB with facts extracted from Git state: staged files, branch families, working-tree status, sensitive-file classification, and approval markers. The wrapper performs string matching and Git inspection in C; the Datalog policy only reasons over already-classified facts. This keeps the policy deterministic, bounded, and auditable.
Classify context in the wrapper
The engine has bounded string filters, including starts_with. Here we deliberately classify paths and branch families in the wrapper so the policy reasons about application concepts such as sensitive_file_staged(File) and branch_class("release"). C and Python must produce the same normalized facts.
Extended domain
static const maelys_datalog_predicate_t git_preds_l3[] = {
{ "operation", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "target_ref", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "source_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 },
/* Level 3: richer context — C extracts, policy decides */
{ "changed_file", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "sensitive_file_staged",1, MAELYS_DATALOG_PREDICATE_EDB },
{ "approval_present", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "working_tree_dirty", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "branch_class", 1, MAELYS_DATALOG_PREDICATE_EDB },
/* POLICY_FACT */
{ "known_operation", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
{ "protected_branch", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
{ "blocked_user", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
{ "release_manager", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
/* sensitive_path is not needed: C classifies files into sensitive_file_staged */
/* IDB */
{ "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 },
};
Extended policy
/* Append these rules to the Level 1 policy. The wrapper classifies paths. */
/* ── Force push ─────────────────────────────────────────────── */
deny("push", "GIT_FORCE_PUSH") :- operation("push"), flag("force").
/* ── Protected branch (non-managers only) ───────────────────── */
deny("push", "GIT_PROTECTED_BRANCH") :-
operation("push"), target_ref(B), protected_branch(B),
user(U), not(release_manager(U)).
/* ── Release branch family (C injects branch_class("release") for release branches) ── */
/* branch_class("release") covers release/v1, release/v2, etc.
protected_branch("release") above covers only the branch named exactly "release".
These two rules are complementary, not redundant. */
deny("push", "GIT_RELEASE_BRANCH") :-
operation("push"),
branch_class("release"),
user(U),
not(release_manager(U)).
/* ── Blocked user ───────────────────────────────────────────── */
deny("commit", "GIT_BLOCKED_USER") :-
operation("commit"), user(U), blocked_user(U).
/* ── Commit message prefix ──────────────────────────────────── */
deny("commit", "GIT_BAD_COMMIT_PREFIX") :-
operation("commit"), commit_msg(M), not(valid_commit_msg(M)).
/* ── Sensitive file requires approval ───────────────────────── */
deny("commit", "GIT_SENSITIVE_FILE") :-
operation("commit"),
sensitive_file_staged(F),
not(approval_present(F)).
/* ── Dirty working tree cannot be pushed ───────────────────── */
deny("push", "GIT_DIRTY_TREE") :-
operation("push"),
working_tree_dirty("true").
/* ── deny_any / allow ───────────────────────────────────────── */
deny_any(Op) :- deny(Op, Code).
allow(Op) :- operation(Op), known_operation(Op), not(deny_any(Op)).
Extended EDB population
typedef int (*path_check_t)(const char *path); /* 1 yes, 0 no, -1 error */
static maelys_datalog_status_t populate_extended_edb(
maelys_datalog_input_edb_t *edb, const git_context_t *ctx,
const char *const *staged, size_t n_staged,
path_check_t is_sensitive_path, path_check_t has_approval,
int working_tree_dirty, maelys_datalog_diagnostic_t *diagnostic)
{
if (!is_sensitive_path || !has_approval || working_tree_dirty < 0)
return MAELYS_DATALOG_STATUS_INVALID_ARGUMENT;
maelys_datalog_status_t rc;
if (ctx->source_ref) {
rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "source_ref", ctx->source_ref);
if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
}
if (ctx->target_ref && strncmp(ctx->target_ref, "release/", 8) == 0) {
rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "branch_class", "release");
if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
}
for (size_t i = 0; i < n_staged; ++i) {
int sensitive = is_sensitive_path(staged[i]);
int approved = has_approval(staged[i]);
if (sensitive < 0 || approved < 0) return MAELYS_DATALOG_STATUS_IO;
rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "changed_file", staged[i]);
if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
if (sensitive) {
rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "sensitive_file_staged", staged[i]);
if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
}
if (approved) {
rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "approval_present", staged[i]);
if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
}
}
if (working_tree_dirty) {
rc = MAELYS_DATALOG_ADD_FACT(edb, diagnostic, "working_tree_dirty", "true");
if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
}
return MAELYS_DATALOG_STATUS_OK;
}Trustworthy identity: not from the environment
The parser callback must supply authenticated identity. Environment variables are attacker-controlled: any
caller can run USER=alice maelys-git push … and impersonate a release
manager. A real gate must derive identity from the operating system, not from
the environment it was handed.
#include <unistd.h>
#include <pwd.h>
/* Real identity: the uid the process runs as, resolved to a name.
Not spoofable through the environment. */
static const char *real_user(void) {
struct passwd *pw = getpwuid(getuid());
return pw ? pw->pw_name : NULL;
}What you have learned
Keep this model
Give policy the repository context it needs- Declare the additional evidence
Staged files, branch classes, tree state and approvals become facts in the extended domain. Rules can then make decisions using that vocabulary.
- Extract and classify outside the rules
The wrapper observes repository state and turns it into canonical facts. Policy uses those facts rather than parsing command-line or path strings.
- Identity needs a trustworthy source
The example obtains the operating-system identity rather than trusting a user-editable environment variable. The wrapper remains responsible for collecting complete, reliable evidence.