maelys-pty
Level 2 — Risk model and confirmation
Classify risk, request confirmation, rebuild the context, and evaluate again.Continue from Level 1 — Line filter with metachar scan. Keep the declarations and helpers from the earlier levels; this page extends the same wrapper in C and Python.
Level 2 adds a risk lattice to the policy. Commands are classified as critical, high, or medium risk. Critical commands are always denied. High-risk commands trigger an interactive confirmation prompt. Medium-risk commands are allowed but logged. The confirmation handler re-evaluates the policy with a fresh EDB after receiving the user’s reply. This catches stale decisions; it does not make checking and execution atomic or close every time-of-check/time-of-use (TOCTOU) race.
Risk lattice
deny(Cmd, Reason) → blocked
challenge(Cmd, Reason) → interactive confirmation prompt
allow_with_log(Cmd, LogMsg) → allowed, decision recorded
allow(Cmd) → allowed silentlyExtended domain
/* Use this complete table instead of pty_preds for Level 2. */
static const maelys_datalog_predicate_t pty_preds_l2[] = {
/* EDB: populated per command line */
{ "command", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "arg", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "user", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "cwd", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "time_of_day", 1, MAELYS_DATALOG_PREDICATE_EDB },
/* Metacharacters detected in the raw line before tokenising */
{ "shell_metachar", 1, MAELYS_DATALOG_PREDICATE_EDB },
/* POLICY_FACT */
{ "forbidden_cmd", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
{ "trusted_user", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
/* IDB + QUERY */
{ "deny_any", 1, MAELYS_DATALOG_PREDICATE_IDB | MAELYS_DATALOG_PREDICATE_QUERY },
{ "allow", 1, MAELYS_DATALOG_PREDICATE_IDB | MAELYS_DATALOG_PREDICATE_QUERY },
{ "deny", 2, MAELYS_DATALOG_PREDICATE_IDB | MAELYS_DATALOG_PREDICATE_QUERY },
{ "risk_critical", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
{ "risk_high", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
{ "risk_medium", 1, MAELYS_DATALOG_PREDICATE_POLICY_FACT },
{ "challenge", 2, MAELYS_DATALOG_PREDICATE_IDB | MAELYS_DATALOG_PREDICATE_QUERY },
{ "allow_with_log", 2, MAELYS_DATALOG_PREDICATE_IDB | MAELYS_DATALOG_PREDICATE_QUERY },
{ "confirmed", 1, MAELYS_DATALOG_PREDICATE_EDB },
{ "challenge_any", 1, MAELYS_DATALOG_PREDICATE_IDB },
};The Level 2 policy
risk_critical("mkfs").
risk_critical("dd").
risk_critical("fdisk").
risk_high("rm").
risk_high("chmod").
risk_high("systemctl").
risk_medium("curl").
risk_medium("wget").
risk_medium("ssh").
deny(Cmd, "critical-risk — out-of-band authorization required") :-
command(Cmd), risk_critical(Cmd).
challenge(Cmd, "high-risk — confirm? [y/N]") :-
command(Cmd), risk_high(Cmd),
user(U), not(trusted_user(U)),
not(deny_any(Cmd)), not(confirmed(Cmd)).
challenge_any(Cmd) :- challenge(Cmd, Reason).
allow_with_log(Cmd, "medium-risk executed") :-
command(Cmd), risk_medium(Cmd),
not(deny_any("__line__")),
not(deny_any(Cmd)), not(challenge_any(Cmd)).
allow(Cmd) :-
command(Cmd),
not(deny_any("__line__")),
not(deny_any(Cmd)),
not(challenge_any(Cmd)),
not(risk_medium(Cmd)).
Confirm, rebuild the context, then evaluate again
The challenge asks the user for confirmation while the shell is paused.
Between the policy evaluation and the user's reply, external state can
change (files created, environment mutated, another process acting).
Rebuild the context after receiving y, record confirmed(Cmd) in that new request, and evaluate again. Confirmation suppresses the challenge, not a denial. Level 2 replaces the Level 1 allow rule; retaining the old rule would bypass the challenge. Retain the Level 1 policy facts, deny rules and deny_any projection. The direct risk_medium test routes medium-risk decisions through logging without adding an unnecessary negation layer; this policy fits the SMALL profile.
/* Application callback: rebuild current evidence, insert confirmed(Cmd),
check all errors, solve, and query allow(Cmd). This is NOT an engine API. */
typedef int (*confirmed_evaluator_t)(void *context, const char *line, size_t len);
static int handle_challenge(void *context, const char *line, size_t len,
confirmed_evaluator_t evaluate_confirmed)
{
if (evaluate_confirmed == NULL) return 0;
fputs("maelys-pty: high-risk — confirm? [y/N] ", stderr);
if (fflush(stderr) != 0) return 0;
char answer[8];
if (fgets(answer, sizeof(answer), stdin) == NULL) return 0;
if (strcmp(answer, "y\n") != 0 && strcmp(answer, "Y\n") != 0) return 0;
return evaluate_confirmed(context, line, len) == 1;
}Level 2 in action
bob@host:~$ rm -rf /tmp/old
maelys-pty: high-risk — confirm? [y/N] y
[re-evaluate: still allowed → forward to shell]
alice@host:~$ dd if=/dev/zero of=/dev/sda
maelys-pty: 'dd' denied — critical-risk — out-of-band authorization requiredWhat you have learned
Keep this model
Treat confirmation as new evidence, not permission to bypass policy- Risk determines the next action
The rules distinguish denial, confirmation, logged permission and ordinary permission. A challenge is not yet an authorization.
- Rebuild the facts after the response
A confirmation adds evidence to a fresh context, which is evaluated again. It must not override an applicable denial.
- Re-evaluation is not atomic execution
Checking again reduces stale decisions but does not eliminate a race between checking and executing. That boundary remains the responsibility of the native monitor.