Policy explain / dry-run 

    basil config explain answers "would this request be allowed, and why?" without performing it. It loads only the catalog and policy JSON (no sealed bundle, no backend I/O, no socket, no secret material), builds the real policy decision point, and evaluates a proposed subject/op/key tuple through the same matcher the live broker enforces with. There is no second copy of the matching logic, so a dry-run can never report a different answer than enforcement; default-deny holds exactly as it does at request time.

    The running broker also exposes the same single-tuple explanation as a gated live admin RPC and top-level basil explain command. Use the offline basil config explain path for pre-deploy review of proposed files; use live basil explain when an operator needs to interrogate the currently serving generation.

    Live explain grant 

    Live explain can enumerate policy reachability, so it is gated by a dedicated broker-admin op. It is not implied by data-plane grants, op:reload, or wildcard *. Grant op:explain explicitly over the reserved target broker.explain:

    {
      "schemaVersion": 2,
      "subjects": {
        "svc.explain": { "allOf": [ { "kind": "unix", "uid": 4242 } ] }
      },
      "rules": [
        { "id": "broker-admin-explain",
          "subjects": ["svc.explain"],
          "action":   ["op:explain"],
          "target":   ["broker.explain"] }
      ]
    }
    services.basil.policy.subjects."svc.explain" = {
      allOf = [ { kind = "unix"; uid = 4242; } ];
    };
    services.basil.policy.rules = [
      { id = "broker-admin-explain";
        subjects = [ "svc.explain" ];
        action    = [ "op:explain" ];
        target    = [ "broker.explain" ]; }
    ];

    Run the command under the granted identity; Basil attests the caller through Unix-socket peer credentials.

    basil explain --subject svc.grafana --op get --key grafana.admin_password --json

    The subcommand & flags 

    basil config explain --catalog catalog.json --policy policy.json \
      --subject svc.grafana --op get --key grafana.admin_password [--json]
    FlagMeaning
    --catalog / --policyThe exported catalog & policy JSON (also honored from a --config TOML or BASIL_CATALOG/BASIL_POLICY). The bundle is not needed.
    --subjectThe registered policy subject to evaluate. Offline explain evaluates the subject name directly; live requests resolve a caller to a subject before enforcement.
    --opOne policy op token (the full list follows this table).
    --keyThe catalog key/target to evaluate.
    --effectivePreview every (key, op) the subject is granted across the whole catalog (ignores --op/--key).
    --jsonEmit a stable machine-readable object instead of human-readable text.

    The --op tokens are: get, list, get_public_key, verify, sign, sign_nats_jwt, validate_nats_jwt, encrypt, decrypt, encrypt_nats_curve, decrypt_nats_curve, mint, validate, set, rotate, import, new_key, use_software_custody, reload, explain, and revoke.

    📝 Subjects are the authorization boundary

    Runtime authorization first resolves the attested caller to a configured subject. The uid, gid, and pid stay as authentication evidence and presenter context in the audit trail; the PDP itself evaluates (subject, op, key).

    Explain evaluates a registered policy subject, not a raw uid/gid principal. --subject, Rust client.explain(subject, op, key), and Go client.Explain(ctx, subject, op, key) all name the subject to evaluate. The --effective view uses the same subject input and returns the grants reachable by that subject, not by a raw Unix principal expression.

    ⚠️ Software custody is an explicit second grant

    For software-custodied PQC keys, explain both the underlying operation and use_software_custody. That op is intentionally not implied by wildcard grants, even root */*, because it authorizes materializing a PQC private seed in the broker process for one operation.

    Allow: the matched rule 

    $ basil config explain --catalog catalog.json --policy policy.json \
        --subject svc.grafana --op get --key grafana.admin_password
    ALLOW  subject svc.grafana  get  grafana.admin_password  (via subject:svc.grafana)
      matched rule `grafana-reader`: action `role:reader` over target `grafana.admin_password`

    When the granting subject definition combines several proofs with allOf, the matched rule still names the canonical subject. The rule provenance tells you which subject the policy granted, not the raw uid/gid predicate that happened to resolve it:

    ALLOW  subject svc.app  sign  app.signing_key  (via subject:svc.app)
      matched rule `app-signer`: action `role:signer` over target `app.signing_key`

    With --json:

    {
      "subject": "svc.grafana",
      "op": "get",
      "key": "grafana.admin_password",
      "decision": "allow",
      "via": "subject:svc.grafana",
      "matched_rule": {
        "rule": "grafana-reader",
        "via": "subject:svc.grafana",
        "action": "role:reader",
        "target": "grafana.admin_password",
        "subject": "svc.grafana"
      }
    }

    A world-readable (class: public) read is allowed with "via": "public_class" and "matched_rule": null. No policy rule produces it.

    Deny: the reason (default-deny) 

    {
      "subject": "svc.unknown",
      "op": "get",
      "key": "grafana.admin_password",
      "decision": "deny",
      "reason": "not_permitted"
    }
    reasonMeaning
    not_permittedNo policy grant matches this (subject, op, key): plain default-deny.
    not_writableA write op against a key whose writable: false: the write hard-cap denies regardless of any policy grant.
    unknown_keyThe key is not in the catalog (reported first, so the tool does not leak which finer check would otherwise have failed).

    JSON schema (single tuple) 

    FieldTypeMeaning
    subjectstringThe evaluated policy subject.
    opstringThe evaluated op token.
    keystringThe evaluated catalog key.
    decisionstringallow or deny.
    viastring(allow only) The scope that granted it: subject:<name> or public_class.
    matched_ruleobject / null(allow only) The granting rule: rule (id), via, action, target, and matched subject. null for a public_class allow.
    reasonstring(deny only) not_permitted / not_writable / unknown_key.

    The --effective view emits {"subject":…,"effective":[{"key","op","via","rule"},…]}; rule is null for a world-readable public-class read.

    ✅ Best practice

    Before merging a catalog/policy change, run basil config explain in CI against the proposed files for the tuples you care about (the service identities that must keep working and the ones that must stay denied), and assert on the --json decision. Because explain runs the identical matcher the broker enforces with, a green dry-run is a real guarantee, not an approximation. Pair it with basil config check so a change that would be rejected at load or would change an authorization outcome is caught pre-merge. Use --effective --json to diff the full granted (key, op) set for an identity across the old and new policy.

    Where to go next 

    • The policy: the rules explain evaluates.
    • Hot reload: apply a reviewed change to the serving generation.
    • Doctor: the environment-level preflight companion.