Command line
============

``ljos-policyd check|exec|capnp|version -- argv...``

``ljos-policyd check-mail --config PATH [--argv [--] RECIPIENT...]``
reads one RFC 5322 message on stdin. See "Outbound mail" below.

============== ======================================================================================
Verb           Meaning
============== ======================================================================================
``check``      print ``allow`` or ``deny<TAB>reason``; exit 2 on deny
``capnp``      write a packed Cap'n ``PolicyDecision`` to stdout; exit 2 on deny
``exec``       same text verdict; on allow, run argv; on deny, print the verdict on stderr and exit 2
``check-mail`` JSON ``allow`` or ``deny`` for one message on stdin; exit 2 when it refuses
``version``    print ``ljos-policyd <semver> (phronesis)`` or ``ljos-policyd <semver> (host table)``
============== ======================================================================================

A leading ``--`` after the verb is optional and is stripped.

Built-in denials
================

First match wins. The head is the last path component of argv[0].
Leading assignments, ``NAME=value`` or ``NAME+=value``, and runner words
such as ``env``, ``nice`` and ``timeout`` come off first, as does the head
of a ``case`` arm (``case $x in a)``, or ``b)`` after ``;;``). A ``trap``
action is judged as a script of its own. A shell with no ``-c`` script
and no file runs what it reads on stdin: the text of a here-string or
heredoc is judged as that script, and a pipe, a ``-s`` shell fed
neither, or a here-string holding an expansion is refused as
``shell-stdin-unshown``. A command word that an ``alias`` earlier on the
line names is read as the alias's body, since dash and zsh expand
aliases in a script and the line does not say which shell reads it.

===================== ======================== ============================
Token                 Cap'n PolicyReason       When
===================== ======================== ============================
``empty argv``        ``emptyArgv``            no program
``sudo``              ``shellPrivilegeDenied`` privilege runner
``curl-pipe-shell``   ``shellRemoteExec``      a download run as a program
``rm-rf-outside-tmp`` ``rmRfOutsideTmp``       recursive delete off tmp
``git-force-push``    ``shellGitDangerous``    force push or guarded lease
``chmod-setuid``      ``chmodSetuid``          setuid or setgid mode
``raw-disk``          ``rawDisk``              dd to a device node, or mkfs
===================== ======================== ============================

Anything else is ``allow``. ``python -c`` is allow. Schema: ``schema/policy.capnp``.

Task runners
============

A line that runs a task runner is also judged on what the target runs.
The recipe comes from the file the runner reads, in the directory it
runs in: the working directory, the one a ``cd`` or ``pushd`` earlier on
the line enters, the one ``env -C`` names for the command it runs, or
the one the runner's own ``-C``, ``--prefix`` or ``--cwd`` names.
``env`` reads its flags as GNU ``env`` does: short ones cluster
(``-iC DIR``, ``-uVAR -C DIR``) and a long one may be cut to a prefix that
names only it (``--ch DIR``), so these move the command as ``-C DIR``
does. ``-S STRING`` (or ``-iS STRING``) splits STRING into words that take
its place, so options in it (``env -S '-C dir' make``) and the words
after it are read too. The shell expands STRING first, so a ``$NAME`` or
``${NAME}`` in it that the line assigned one value earlier, and sets no
other way, is read as that value. A string that still holds a ``$`` after
that may run any command, so it is refused as
``env-split-string-unshown``.
Leading words that still run ``cd`` in this shell (``command``,
``builtin``, ``if``, ``while``, ``{`` and the other control keywords, a
``case`` arm's head, and
the ``time`` keyword with its ``-p``, which is a program after ``command``
or ``builtin``, where no word is a keyword)
are peeled before this check, as are assignments among them
(``if FOO=1 cd dir``), and a redirection among the words of a
``cd`` or ``pushd`` names neither the directory nor a ``pushd`` stack index.
One that closes a descriptor (``2>&-``, ``<&-``) is a whole word, so the
word after it is the target.

============================================ ==============================================================================================
Runner                                       File read
============================================ ==============================================================================================
``make``, ``gmake``                          ``GNUmakefile``, ``makefile`` or ``Makefile``, or ``-f FILE``, with the target's prerequisites
``just``                                     ``justfile``, ``Justfile`` or ``.justfile``
``cargo make``, ``makers``                   ``Makefile.toml``
``npm run``, ``pnpm``, ``yarn``, ``bun run`` the ``scripts`` of ``package.json``, with ``pre`` and ``post``
============================================ ==============================================================================================

Each recipe line is judged as a ``sh -c`` script, and a refusal names the
target and the line: :literal:`make clean-all runs \`rm -rf ~\`: rm-rf-outside-tmp`.
Nothing is spawned. A missing or unreadable file leaves the line
judged as written. A ``cd`` or ``pushd`` to a path the line does not show
(``cd "$X"``) leaves it in a directory it does not follow, so a runner
after it is refused. So does an ``eval``, a ``source`` or ``.`` whose file
the guard cannot read or whose text may move the shell or set one of
those, a command word the line does not show (``$c dir``), which may be
``cd``, as may an alias whose body the line does not show, and a ``trap``
whose action is one of those or a ``cd``, ``pushd`` or ``popd``, since a
``DEBUG`` trap runs before each command; after one ``HOME``, ``CDPATH`` and
``PWD`` are not followed either. A ``source`` or ``.`` of a literal path to
a regular file the guard can read, whose text does none of that, leaves
the line where it was. The text is read with the aliases the line
defined before the ``source`` expanded, as the shell that sources it
reads it.
Raw newlines and carriage returns split script commands, and ``$'...'``
ANSI-C quoted words are decoded before the line is judged. A backslash
before a newline joins the two lines, outside single quotes, as the
shell joins them.

A ``cd`` or ``pushd`` to a relative path whose first part is not ``.`` or
``..`` enters that path under the first ``CDPATH`` entry that holds it, an
empty or ``.`` entry being the working directory, so the runner after it
is read there, and in any entry before it the line does not show. A
``CDPATH`` set in
front of the ``cd`` (``CDPATH=dir cd x``), or of the shell that runs it, is
the one searched, as is one the line sets before it, alone or with
``export``, ``declare`` and the like (``export CDPATH=dir; cd x``), unless a
pipeline stage, a subshell or a list sent to the background set it.
One set where it may not run, past a ``&&`` or ``||``, is searched along
with the one before it. Another command that writes it in this shell
(``read CDPATH``, ``printf -v CDPATH``, ``${CDPATH:=dir}``, or an
assignment in front of a special builtin such as ``:``), or an entry with
``$``, a backquote or ``~`` in it, names no directory the line shows, so
a runner after the ``cd`` is refused. A word that only names it
(``grep CDPATH``, ``env CDPATH=dir cmd``) changes nothing.

``HOME`` is followed the same way: a bare ``cd`` enters the one set in
front of it (``HOME=dir cd``) or earlier on the line (``export HOME=dir; cd``), and ``~`` expands from the one set before the command, as do
``$HOME`` and ``${HOME}`` at the head of a path. A ``HOME``
that may hold either of two values, or one the line does not show
(``HOME=$(mktemp -d) cd``), leaves the runner after the ``cd`` refused.
``$PWD`` in either value is the one directory the line is in, when it
shows that and does not set ``PWD`` (``export HOME``"$PWD/.home"``). With =HOME`` unset, ``~`` is the home in the user's passwd entry, as in
bash, and a bare ``cd`` stays. With it unset or empty, ``$HOME/path`` is
the path from ``/``, as is ``~/path`` with it empty, while
``cd $HOME``, or ``cd ~`` with it empty, stays.
``~login`` expands from the password database and ``~+`` to where the
line is, while ``~-`` and a stack entry such as ``~1`` name a directory
that is not followed.

A ``cd`` or ``pushd`` takes ``..`` off the path by name, as the shell's
logical ``cd`` does, so ``cd lnk/..`` comes back to where it started even
when ``lnk`` is a symbolic link. Where that names no directory, the
line may instead be through the link, as bash then tries the path as
written (``cd lnk/../x`` with ``x`` only beside the link's target).
``cd -P``, the last of ``-L`` and ``-P`` winning, resolves the link
first, and after a ``set -P`` or ``set -o physical`` on the line, or a
``SHELLOPTS`` that may name ``physical`` for a bash the line starts, either
may hold. A ``source`` or ``.`` whose file runs ``set -P`` leaves the line in
a directory it does not follow. ``env -C`` resolves it, as ``chdir`` does.

``pushd`` and ``popd`` follow the directory stack. ``pushd -n`` stacks its
directory without entering it, ``pushd +N`` and ``-N`` enter the entry they
bring to the top, and a bare ``popd`` enters the entry under the top,
while ``popd -n``, ``popd +N`` and ``popd -N`` drop an entry and stay. An
index that counts past a ``pushd`` that may have failed may name any
directory on the stack. A ``popd`` that fails, on a word it does not take
(``popd x``) or with nothing left to pop (after ``dirs -c``), leaves the
line in a directory it does not follow. Under ``shopt -s lastpipe`` or
``set -o lastpipe``, alone or in an ``eval``, or a ``shopt`` with a word
the line does not show (``shopt -s $opt``), and in a shell the line
starts after setting ``BASHOPTS`` or ``SHELLOPTS`` to a value that names
``lastpipe`` or that it does not show, a
``cd``, ``pushd`` or ``popd`` in the last stage of a pipeline does the same,
and a ``dirs`` there leaves the stack not followed.

The line goes on past a ``cd`` that may fail, so the runner after it is
read in each directory the line may be in, up to sixteen. A ``cd`` into a
directory without the search bit moves nothing. A runner that may run
in a directory past those sixteen is refused, as
``make clean-all may run in an untracked directory``.

A ``cd -`` or ``pushd -`` enters ``OLDPWD``, which each ``cd``, ``pushd`` and
``popd`` on the line moves, and a script the line runs starts with the
one its command has. Without ``OLDPWD`` in the environment, and in a
recipe's line, where it is not followed, a runner after ``cd -`` is
refused the same way.

The words ``xargs`` adds from stdin can name any target, so a runner that
``xargs`` runs is refused, as ``xargs make may take a target from stdin``.

Argument limits
===============

A phronesis build hands each word to the pack whole. phronesis 0.2.3
keeps a word of up to 128 KiB (131072 bytes) and up to 32768 words. A
Cap'n command body over 512 KiB (524288 bytes) is refused before the
word count is checked. Past a limit the answer is ``field-too-long`` and
the host's reason:

======================================= =====================================================
Past                                    Answer
======================================= =====================================================
a word longer than 131072 bytes         ``field-too-long: argv word exceeds 131072 bytes``
more than 32768 words                   ``field-too-long: argv count exceeds 32768``
a command body longer than 524288 bytes ``field-too-long: command body exceeds 524288 bytes``
======================================= =====================================================

A blank reason is the name alone, ``field-too-long``. The limits are
``PHRONESIS_SHELL_WORD_MAX`` and ``PHRONESIS_SHELL_ARGV_MAX`` in phronesis
``src/internal.h``, and ``PHRONESIS_CAPNP_MAX_BODY`` in phronesis
``include/phronesis/supervisor.h``. phronesis 0.2.3 sets them to 131072,
32768 and 524288 and writes the reason strings above. This binary names
the code and keeps the reason. Release binaries link phronesis 0.2.5.
A build without the library uses the host table and does not apply
these limits.

Outbound mail
=============

``check-mail`` prints one JSON object and only refuses. ``allow`` means
this check found nothing to refuse. A send still needs its own grant.

The message on stdin is the whole input. A caller must pass no
recipient arguments to ``sendmail -t``: those addresses are not in
the message. Pass the same recipients to ``--argv`` and this check
refuses them as recipients. ``--config`` comes before ``--argv``.

The config is lines of ``identity``, ``deny-file`` and ``pack-law``,
an optional ``subaddress`` line, and any number of ``alias`` lines. A
relative path is read from the config file's directory. ``identity`` is
the one address ``From`` must be, after ASCII case folding only. A
local part or domain that NFKC or the removal of a skipped
character would change is a refusal, including a zero-width
character or a fullwidth spelling. The mailbox is the address in
angle brackets, read from the raw unfolded value. A display-name encoded-word is decoded for display only, and
an encoded addr-spec is a refusal. A second ``From`` is a refusal.
A group in ``From`` is a refusal. A group in a recipient header
still contributes its mailboxes.
``Reply-To``, ``Sender``, ``Return-Path``, ``Errors-To``,
``Disposition-Notification-To``, ``Return-Receipt-To``,
``Delivery-Receipt-To``, ``Mail-Followup-To`` and ``Mail-Reply-To``
must stay clear of a deny-file identity (``address``, or a mailbox
at a ``domain``) and of a domain named by the pack-law file.
``Sender``, ``Errors-To`` and ``Return-Path``, when present, must
be the identity under the same rule as ``From``. An empty
``Return-Path`` is not that address. Any ``Resent-*`` header is a refusal, and so is
any ``Apparently-To``. ``To``, ``Cc`` and ``Bcc`` are refused when
the mailbox is an ``address`` line in the deny file, and when the
domain or a parent of it is named by the deny file or by a
``deny-domain`` line in the pack-law file. An ``address`` line is
that mailbox, not the whole domain. It also covers that local
part, one ``subaddress`` separator, and anything after the
separator. The separators are the ``subaddress`` line, or ``+-``
when the line is omitted. ``.``, ``@``, ``%`` and ``!`` are not
separators. An ``address`` rule is also compared by skeleton:
NFD, combining marks removed, case folded twice, combining marks
removed again, then NFC, with every ``.``, ``_``, ``-``, ``+`` and ``=``
removed and nothing cut. The second fold turns capital sharp s
into ss. The second removal drops the dot that case folding İ
produces, so İ and i are one skeleton, and a mark cannot compose
with the letter before it and escape the prefix. ``abc`` followed
by U+0301 and ``x`` is denied by ``abc``, and Lithuanian i + U+0307 +
U+0300 is the same skeleton as ì. A recipient whose skeleton starts with
the rule's skeleton, at the rule's domain or one in the same
alias family, is refused, and so is one the folding below
refuses. This over-blocks on purpose: a rule for ``john`` also
refuses ``johnny`` and ``jo.hn-x``, so a provider that ignores a
character no family lists cannot deliver a denied mailbox.
An address rule whose skeleton is empty refuses the deny file.
The error names the line and not the address. A deny file that
does not load refuses every send. A skeleton shorter than three
characters still matches that mailbox, its subaddresses and its
alias family, and does not use the prefix, so ``jo`` denies ``jo``
and ``jo+tag`` and not ``john``.
A provider alias family folds both the rule and the
recipient, and adds its separators to the ``subaddress`` line on
its domains. ``gmail.com`` and ``googlemail.com`` are one
domain, dots in the local part are ignored, and ``+`` is a
separator. On ``fastmail.com``, ``+`` is a separator and a
mailbox at ``<local>@<user>.fastmail.com`` is
``<user>+<local>@fastmail.com``. Every label in front of
``fastmail.com`` is the user, and a hyphen there is part of the
user name. A punycode user label is decoded to Unicode and folded as a
local part, so ``x@xn--bcher-kva.fastmail.com`` is
``bücher+x@fastmail.com``. ``proton.me``,
``protonmail.com``, ``protonmail.ch`` and ``pm.me`` are one
domain, and so are ``icloud.com``, ``me.com`` and ``mac.com``,
each with ``+``. ``outlook.*``, ``hotmail.*``, ``live.*`` and
``msn.*`` (every domain with that first label, whatever the
country suffix) are separate domains, each with ``+``. Exact and
subaddress matching stay on the domain the rule names. The
skeleton prefix still crosses that family, so a rule for
``john@outlook.com`` also denies ``john@hotmail.com``,
``john@live.com``, ``john@msn.com`` and ``john@outlook.de``. That
over-block is the prefix, not a shared mailbox. On
``yahoo.*``, ``ymail.*`` and ``rocketmail.com``, ``-`` is a
separator, and an address rule
is also cut at its first separator, so a rule for one disposable
address ``<base>-<keyword>`` covers ``<base>`` and every keyword.
An ``alias`` line adds a family to these:
``alias <domain>[,<domain>...] <separators> [flag...]``. A
domain may be ``<label>.*``, every domain whose first label is
``<label>``. The separators are checked as on the ``subaddress`` line, and the flags
are ``shared`` (the domains, and the same subdomain of each, are
one mailbox), ``dots`` (dots in the local part are ignored),
``subdomain`` (``<local>@<user>.<domain>`` is
``<user><sep><local>@<domain>``, ``<sep>`` being the line's first
separator, folded again so ``dots`` also reaches the user) and ``cut`` (the rule is also cut at its
first separator). An ``alias`` line cannot remove a built-in
family, and one that does not parse, or names one domain or flag
twice, refuses the config. A
``domain`` rule on a domain of a ``shared`` family covers every
domain of that family and their subdomains, so ``domain gmail.com``
also refuses ``googlemail.com``, ``domain pm.me`` the other Proton
domains, and ``domain icloud.com`` ``me.com`` and ``mac.com``.
``deny-domain`` in the pack-law file does the same. A ``domain``
or ``deny-domain`` value has to be a hostname after IDNA. A star,
a colon, a slash, an at-sign, whitespace, or anything else that
is not a hostname refuses the file. So does a domain a
recipient cannot have, such as an all-numeric one, in a
``domain``, ``deny-domain`` or ``address`` value. The detail is the
line number and not the value. A wildcard is not how to name
subdomains: ``domain example.test`` already covers
``a.example.test``. An ``address`` value with a star, or with a
character a parsed recipient cannot carry, is refused the same
way. ``From`` is
not matched this way. A domain literal (an address
at ``@[ ... ]``) in a watched header is a refusal. So is a domain
that is not a hostname after IDNA, such as one with an underscore,
a ``!`` or a ``+``, a label that starts or ends with a hyphen, a label
over 63 characters or a name over 253: no rule could name it. An
all-numeric domain is a refusal, judged after IDNA. That
includes a dotted quad, a shorter form inet\ :sub:`aton` accepts, a
hex label (``0x``) and a label with a leading zero. U+3002 or
U+FF61 between the numbers is still that address. A source route
(``<@host:mailbox>``) is a refusal. ``@``, ``%`` or ``!`` in a local
part is a refusal, quoted or not, including a backslash escape
inside the quotes. A local part that NFKC changes into ASCII
punctuation, such as a fullwidth ``@``, ``%``, ``!`` or ``+``, is a
refusal. A local part that contains a default ignorable code point,
a noncharacter, an unassigned or private-use code point, a format
or control character, or a space separator is a refusal. A
zero-width space or a braille blank is still removed before the
address is compared.
A recipient domain, or a domain on a header checked against the
deny file, is refused when one label mixes Latin with Cyrillic or
Greek after NFKC and IDNA.

A deny-file domain compares a fullwidth spelling and the punycode
form as the same domain. A homoglyph that NFKC and IDNA leave alone
does not, and a label wholly in Cyrillic or Greek is not mapped to
the Latin it looks like. Case is folded as the address skeleton
folds it, so ß is ss and ı and İ are i, and a character a reader
skips is removed before that comparison. ``From`` is not compared that way. ``handle``, ``term`` and ``pair`` may share the deny file and
are ignored here. A pack-law line that would allow a domain refuses
the file. The detail of ``deny-file-invalid`` and ``pack-law-invalid``
is the line number, not the value. A message over 1 MiB, a control character in the header
block, U+0085, U+2028 or U+2029 in that block, a header line over
998 octets, a header block that mixes CRLF and a bare LF, or a
config that cannot be read is a refusal.

======================================== ================================================================================================================================
Code                                     When
======================================== ================================================================================================================================
``from-missing``                         no From
``duplicate-from``                       more than one From
``from-not-identity``                    From is not the configured identity
``from-unparsed``                        From does not parse as one mailbox
``from-group``                           From uses group syntax
``encoded-word``                         an encoded-word in a watched header did not decode
``reply-to-denied``                      Reply-To names a deny-file identity
``reply-to-unparsed``                    Reply-To does not parse
``sender-denied``                        Sender names a deny-file identity
``sender-unparsed``                      Sender does not parse
``sender-not-identity``                  Sender is present and is not the identity
``return-path-denied``                   Return-Path names a deny-file identity
``return-path-unparsed``                 Return-Path does not parse
``return-path-not-identity``             Return-Path is present and is not the identity
``errors-to-denied``                     Errors-To names a deny-file identity
``errors-to-unparsed``                   Errors-To does not parse
``errors-to-not-identity``               Errors-To is present and is not the identity
``disposition-notification-to-denied``   Disposition-Notification-To names a deny-file identity
``disposition-notification-to-unparsed`` Disposition-Notification-To does not parse
``return-receipt-to-denied``             Return-Receipt-To names a deny-file identity
``return-receipt-to-unparsed``           Return-Receipt-To does not parse
``delivery-receipt-to-denied``           Delivery-Receipt-To names a deny-file identity
``delivery-receipt-to-unparsed``         Delivery-Receipt-To does not parse
``mail-followup-to-denied``              Mail-Followup-To names a deny-file identity
``mail-followup-to-unparsed``            Mail-Followup-To does not parse
``mail-reply-to-denied``                 Mail-Reply-To names a deny-file identity
``mail-reply-to-unparsed``               Mail-Reply-To does not parse
``apparently-to``                        an Apparently-To header is present
``resent-header``                        a Resent-\* header is present
``mixed-script``                         a label mixes Latin with Cyrillic or Greek
``recipient-address-denied``             To, Cc or Bcc names a deny-file address
``recipient-domain-denied``              To, Cc or Bcc is at a denied domain
``recipient-unparsed``                   To, Cc or Bcc does not parse
``argv-recipient-denied``                an argv recipient is a denied address or domain
``argv-recipient-unparsed``              an argv recipient does not parse
``header-control``                       a control, or U+0085, U+2028 or U+2029
``header-line-ending``                   the header block mixes CRLF and a bare LF
``header-line-too-long``                 a header line is over 998 octets
``message-too-large``                    the message is over 1 MiB
``message-invalid``                      the bytes are not a header block
``config-unreadable``                    the config path cannot be read
``config-invalid``                       the config is missing a key or names an unknown one
``identity-invalid``                     the configured identity is not one mailbox
``deny-file-unreadable``                 the deny file cannot be read
``deny-file-invalid``                    a deny-file line is not a known kind, a domain is not a hostname, or an address skeleton is empty. The detail is the line number
``pack-law-unreadable``                  the pack-law file cannot be read
``pack-law-invalid``                     a pack-law line is not deny-domain, or the domain is not a hostname. The detail is the line number
======================================== ================================================================================================================================

Environment the seat reads
==========================

These are not read by this binary. They are how ``ljos`` finds it.

==================== ===========================================
Variable             Meaning
==================== ===========================================
``POLICYD_BIN``      absolute path to this binary
``POLICYD_REQUIRED`` if set to ``1``, a missing binary is a deny
==================== ===========================================

Exit statuses
=============

===== ================================================================================
Code  Meaning
===== ================================================================================
0     allow, check-mail with nothing to refuse, or ``exec`` of a process that exited 0
2     deny, a check-mail refusal, missing verb, or empty usage
127   ``exec`` could not spawn the program
other ``exec`` of a process that exited with that code
===== ================================================================================

What this crate is
==================

- Remember and Prefer stay in packset.
- A check is the argv verdict. Pack rules stay in packset.
- ``ljos policy`` prints the line and composes this verdict with pack
  rules.
