Did you know ... Search Documentation:
Pack hornguard -- prolog/hornguard_worker.pl
PublicShow source

The pack in its own process. A host of any language spawns

swipl prolog/hornguard_worker_main.pl        (or: make worker)

and exchanges one JSON object per line over stdin/stdout. The worker reads author text itself, under the backend's reader flags and with the standard operator table, so the untrusted engine never parses author text: an admitted term comes back in canonical form, and only that form should cross to the engine. Reader-level hazards (syntax errors, quasi-quotations, oversized input, more than one term where one is expected) are refused under the evasion class with a reader(Reason) rule.

Handshake: on start the worker writes

{"hello":"hornguard","protocol":1,"engine":"swi","version":"9.2.9",
 "profiles":[...],"profiles_engine":"9.2.9","engine_matches_profiles":true}

profiles_engine is the engine version the generated swi profile records; an attestation of purity is per engine version, so a host running on another one is told at the handshake.

Requests carry an id (echoed), an op, and op-specific fields:

{"id":1,"op":"judge_goal","text":"findall(X, member(X,[a]), L)",
 "backend":"swi","profiles":["iso","prologue"],
 "options":{"strict_negation":true,"defer_unknown":false,
            "allow":["foo/2"],"trust":[["bar/3","none"]],
            "dynamic_dispatch":"judged","defining":["my_battery"]}}
{"id":2,"op":"judge_clause","text":"p(X) :- q(X)."}
{"id":3,"op":"judge_program","text":"p(1).\np(X) :- q(X)."}
{"id":4,"op":"load_policy","path":"/etc/host/policy.pl"}
{"id":5,"op":"load_profiles","dirs":["/a/profiles","/b/profiles"]}
{"id":6,"op":"profiles"}
{"id":7,"op":"ping"}

backend, profiles and options are optional; absent, the loaded policy applies. Responses:

{"id":1,"verdict":"admit","canonical":"findall(A,member(A,[a]),B)"}
{"id":1,"verdict":"admit_with","canonical":"hornguard_call(G)"}
{"id":1,"verdict":"admit_needs","needs":[{"profile":"swi"}],"canonical":"..."}
{"id":1,"verdict":"refused","class":"escape_attempt","rule":"pinned(process)",
 "depth":1,"reason":"permission_error(execute,goal,shell/1)"}
{"id":4,"ok":true}
{"id":9,"error":"unknown_op","detail":"..."}

The worker is sequential; hosts wanting parallelism run several.

 apply_ops is det
Give this module's reader the operators the loaded profiles declare, so author text that uses a host's operator (a battery's ::) reads. The canonical form is operator-free, so the engine never needs them; op/3 stays pinned for authors. Called at start and after load_profiles.
 hornguard_judge_text(+Op, +Text, +Options, -Response) is det
Op is judge_goal | judge_clause | judge_program. Options carry backend(B), profiles(Ps) and the judge options. Response is a dict with the verdict fields described in the module header.
 hornguard_read(+Backend, +Text, -Terms, -Report) is det
 hornguard_read(+Backend, +Text, -Terms, -VarNames, -Report) is det
Read author text under the backend's reader flags, with the standard operator table (this module defines no operators), refusing quasi-quotations and oversized input. Report is ok or refused(reader(Why)). VarNames has one Name=Var list per term, so the canonical form can keep the author's variable names and a host can map bindings back.
 hornguard_canonical(+Term, -Text) is det
 hornguard_canonical(+Term, +VarNames, -Text) is det
Operator-free canonical text: every compound in functional notation, atoms quoted where needed. With VarNames (Name=Var pairs from the reader) the author's variable names are kept, so a host can map the engine's bindings back to them; every other variable is anonymous and prints as _. Without names, variables are lettered in order of appearance and singletons print as _. Only this form should cross to the engine.

Undocumented predicates

The following predicates are exported, but not or incorrectly documented.

 hornguard_worker_main
 hornguard_read(Arg1, Arg2, Arg3, Arg4, Arg5)
 hornguard_canonical(Arg1, Arg2, Arg3)