| Did you know ... | Search Documentation: |
| Pack hornguard -- prolog/hornguard.pl |
The judge. Pure: it never executes the term it is given. It walks a goal or a clause, consults the loaded profiles (allow/2, meta_spec/2) and the pinned class table (pinned/2), and returns a verdict:
admitadmit_with(Guarded) - under dynamic_dispatch(judged): admissible
admit_needs(Needs) - admissible once the host satisfies each
profile(Name) for a known profile
not in force, predicate(Indicator) for a
sandboxed predicate not yet stored (only
under defer_unknown(true))
refused(Reason, Class, Rule)
Reason: an ISO error term the host may show the author, subject to
its oracle policy
Class: benign_miss | capability_probe | escape_attempt | reconnaissance
| shadowing | semantics | evasion
shadowing is the head family: a clause that would stand in
for a definition the judge reasons about. Its body is walked
like any other; the harm is to what the host's predicate
answers, not to what a call can do, and in practice it is
the class a policy gap shows up as (see tools/triage.pl).
Rule: pinned(Class) | unbound_goal | qualified | meta_spec(Indicator)
| unknown | not_callable | head(Why) | directive | unsupported(What)
| evaluable(Name/Arity)
| unstratified(Members, Head-Callee) | floundering(NegatedGoal)
| cyclic_term | term_depth
A pinned rule nested inside a meta-argument carries + depth(N).
head(Why) is one of control, pinned(Class), profile(Name),
trusted, qualified.
The semantics class is not a threat signal. It marks a program the judge
can admit capability-wise but refuses to store because it has no single
intended meaning: recursion through negation or aggregation
(hornguard_stratification/2), and negation used as if it bound a variable
(hornguard_floundering/2). The latter is refused only under
strict_negation(true), the default; a host that passes
strict_negation(false) may still call hornguard_floundering/2 itself and
warn.
Structural rules, in the order the walk applies them:
trust(Indicator, Spec) is admitted with
its declared meta spec applied and its body not walked.defer_unknown(true) so is a predicate the engine does not define.Two more rules apply where the walk would otherwise not look. An arithmetic predicate's expressions are checked for pinned evaluables (the clock-reading functions), since arithmetic is a second language inside the first. And a clause head may not name a predicate the host trusts: the trusted definition is the one whose body is never walked, and a clause in sandboxed space would stand in for it.
Rule 1 has one sanctioned relaxation. Under dynamic_dispatch(judged) an
unbound goal or closure is not refused but rewritten to hornguard_call/N,
which judges the goal under the same policy at the moment it runs and only
then calls it, and catch/3 becomes hornguard_catch/3, which cannot swallow a
runtime refusal. The verdict is then admit_with(Guarded), and the host runs
Guarded. Judgment happens twice, statically where the goal is known and at
the sink where it is not; nothing runs unjudged either way. The runtime
judge is the loaded policy plus whatever hornguard_set_runtime_context/1 has
set (a namespace's stored predicates, typically), or a host's own
runtime_judge_hook/2 when it wants the judging done in a process
the author cannot reach.
Enforcement (hornguard_run/4) is not yet implemented; it belongs to the backend layer, not the judge.
The directory holds profiles and backend manifests. Accepted terms:
allow(Profile, Name/Arity), meta_spec(Profile, Spec),
pinned(Class, Name/Arity), pinned_evaluable(Class, Name/Arity),
engine(Backend, Name/Arity), enforcement(Backend, Kind) and directives
(ignored). Anything else is a domain_error. An allow that names a pinned
indicator is a load error: pinned classes are not reopened by profile.
Every file is read and checked before any table changes, so a directory that fails to load leaves the profiles that were in force exactly as they were.
op(Priority, Type, Name)
terms. A host whose stored rules use an operator (a battery's ::,
say) declares it in its profiles directory; the judge worker's reader
honours it, and the canonical form it emits is operator-free, so the
engine never needs to know. op/3 itself stays pinned for authors.backend(Name) default isoprofiles(List) profiles in force; default [iso]option(Opt) strict_negation(B) | defer_unknown(B)allow(Name/Arity) a sandboxed predicate whose clauses
were body-judged at storagetrust(Name/Arity, Spec) a host predicate admitted without
none or a
meta_predicate-style term of the same
name and arity
author_defines(Name/Arity) a host predicate authors may add
unpin(Class) reopen a pinned class. Logged as a
warning at load, every time.Load errors are thrown: an allow that names a pinned indicator (unless that class is unpinned in the same file), a trust spec whose name or arity does not match, an unknown profile, an unknown option, or any other term. Loading replaces the previous policy.
policy(Backend, Profiles, Options, Allow, Trust),
or the default policy(iso, [iso], [], [], []) when none is loaded.allow(ListOfIndicators)
Host predicates whose clauses live in sandboxed space and were
body-judged at storage. Admitted like a profile entry, no spec.none.strict_negation(Bool)
Default true: a negated goal that introduces a variable used later
is refused under semantics. False skips the refusal.defer_unknown(Bool)
Default false. True reports an indicator the engine does not define
as predicate(Indicator) in admit_needs instead of refusing it, so a
stored rule may call a rule stored later. Engine-defined predicates
no profile allows are still refused.stratified(Strata), Strata a list of lists of indicators
from the lowest stratum up, or unstratified(Members, Head-Callee):
the strongly connected predicates that recurse through a negative
dependency, and the negative edge that closes the cycle.
Dependencies are collected from rule bodies. Control constructs are
transparent. \+, not/1, forall/2, and the all-solutions and
aggregation predicates (findall, bagof, setof, aggregate_all)
make their goal arguments negative dependencies, as Datalog treats
aggregation, because their result depends on the callee being complete.
Other meta-predicates pass the current polarity to their goal and
closure arguments. Only predicates defined in Clauses take part;
everything else is a base relation. Directives and DCG rules are
ignored here (hornguard_admit_program/5 refuses them first).
\+ G and not(G) goals in the clause body (or
in the goal, read as a body with no head) that introduce a variable and
then rely on it: the variable does not occur in the head or in any
earlier positive goal, and does occur somewhere after the negation.
Negation never binds, so such a variable is unbound where it is used.
A variable that occurs only inside the negated goal is existential and
fine (\+ parent(_, X)). Aggregation goals (findall/3 and friends)
bind only their result argument; their template and goal variables are
local and do not count as bound afterwards. Disjunction is read
permissively: an occurrence in any earlier branch counts as bound.
dynamic_dispatch(judged)
does this itself and returns the result in admit_with/1; this is the
same rewrite for a host that wants it separately.error(Reason, hornguard(Class, runtime(Rule)))
which hornguard_catch/3 will not swallow. A host that wants the judging done outside the engine defines runtime_judge_hook/2.
profiles(Names), allow(Indicators), trust(Pairs). A host sets this for
the namespace whose rules are about to run, from a position the author
cannot reach; the setter is not in any profile.The following predicates are exported, but not or incorrectly documented.