maelys-git

Level 4 — Per-repository policy

Load a repository-local policy through a manifest and verify its digest.

Continue from Level 3 — Rich Git context. Keep the declarations and helpers from the earlier levels; this page extends the same wrapper in C and Python.

Level 4 moves the policy from compiled-in source to a repository-local .maelys/git/ directory. Each repository can carry its own manifest, policy file, SHA verification, and optional signature. Policy updates become normal reviewed commits or pull requests, and a tampered policy fails closed at load time.

Repository layout

CODE
.maelys/
  git/
    manifest.json      ← describes the policy: id, domain, SHA, mode
    policy.dl          ← the Datalog rules
    policy.sig         ← detached signature (optional, out of scope here)

Manifest

Save the Level 1 policy as .maelys/git/policy.dl in UTF-8 with LF endings and one final newline. This manifest uses its exact digest; recompute it whenever the policy bytes change. deny_any is internal and is not a public query.

CODE
{
  "policy_set_id": "git-gate",
  "policy_set_version": "1",
  "manifest_version": "1",
  "default_profile": "enforce",
  "created_for": "maelys-git-tutorial",
  "strict_loading": true,
  "fail_closed": true,
  "capabilities": [],
  "policies": [
    {
      "policy_id": "git_policy.main",
      "domain": "git_policy",
      "file": "policy.dl",
      "sha256": "1ba2ffc80e876eb8b3d8effd6433c538624250fb7f7c16894e549a16b0185d5a",
      "mode": "enforce",
      "enabled": true,
      "description": "Git operation gate",
      "queries": [
        {
          "name": "allow",
          "arity": 1
        },
        {
          "name": "deny",
          "arity": 2
        }
      ]
    }
  ]
}

Loading at startup

CODELoad a repository manifest
static maelys_datalog_status_t load_repo_policy(
    const char *manifest_path, maelys_datalog_session_t **session,
    maelys_datalog_diagnostic_t *diagnostic)
{
    maelys_datalog_policy_t *policy = NULL;
    maelys_datalog_status_t rc = maelys_datalog_policy_load_manifest(
        manifest_path, MAELYS_DATALOG_PUBLIC_ALLOW_NONE, &policy, diagnostic);
    if (rc != MAELYS_DATALOG_STATUS_OK) return rc;
    size_t count = 0;
    rc = maelys_datalog_policy_count(policy, &count);
    if (rc == MAELYS_DATALOG_STATUS_OK && count != 1u)
        rc = MAELYS_DATALOG_STATUS_INVALID_FIELD;
    if (rc == MAELYS_DATALOG_STATUS_OK)
        rc = maelys_datalog_session_create(policy, 0u, session);
    (void)maelys_datalog_policy_free(policy);
    return rc;
}

The manifest loader checks policy.dl against its declared SHA-256. A mismatch fails loading. The digest does not authenticate a manifest that an attacker can also replace; protect the package or verify its signature separately. For Level 3, register git_preds_l3 instead of git_preds and extend POLICY_ATOMS with GIT_RELEASE_BRANCH, GIT_SENSITIVE_FILE, GIT_DIRTY_TREE and true. Update the file digest after adding the rules.

Updating the policy

CODE
vim .maelys/git/policy.dl
sha256sum .maelys/git/policy.dl | awk '{print $1}'   # update sha256 in manifest
vim .maelys/git/manifest.json
git add .maelys/git/
git commit -m "policy: require approval for sensitive files"

Policy changes are PRs. The SHA-256 ties the manifest to the exact policy bytes that reviewers approved.

What you have learned

Keep this model

Keep a reviewed policy alongside the repository
  • Rules live outside the executable

    The wrapper loads .maelys/git/policy.dl through a manifest, so policy changes need not be embedded in the wrapper source.

  • The digest binds the manifest to the file

    The loader checks the policy bytes against the declared SHA-256 before accepting them. A reviewed policy update also requires an updated digest.

  • Integrity still depends on trust

    An attacker who can replace both the policy and its manifest can choose a matching digest. Protect or authenticate the package through a separate trusted mechanism.

Continue