Cavil-CLI
view release on metacpan or search on metacpan
docs/Architecture.md view on Meta::CPAN
## What it does, and what it does not
`check` uploads the working tree, waits for Cavil to review it, and reports the licensing risk, turning it into
a pass or fail for CI. It is the whole product; `whoami` and `config` only exist to make it usable.
It does not do its own license detection, and it does not clear or sign off anything. Acceptability is a
decision the review workflow owns; the CLI reports the risk the review found and gates on it, and leaves the
human judgement where it belongs.
Two things are deliberately out of scope for now. There is no lighter, backlog-free "just tell me about this
throwaway code" path yet; a submission today is a real review that enters the real queue, which is why it is
gated to high access (below). A separate sandbox namespace for ordinary users is planned for that, and the
client is shaped so it can grow one without changing how `check` looks.
## The commands
The work is one command. You run the check, you read the report, and in CI you look at the exit code. Keeping
it to a single command with a built-in gate is a deliberate reaction to the friction of provenance pipelines
where a scan produces a raw file that further tools must render and police before it means anything.
Two small helpers stand beside it. `whoami` asks the server who the token belongs to and times the round trip,
so a user can confirm the URL and token work and the instance is reachable before running a real check, and CI
can use it as a preflight. `config` saves the server URL and API token so they need not be passed every run;
the token is read from a hidden prompt (or piped in), never taken as a command-line flag where it would linger
in shell history and process listings, and it is stored under `~/.config/cavil-cli`. Credentials are resolved
from the environment (for CI) or that saved config, always as a pair from one source, never mixed: taking the
URL from one place and the token from another is how a token saved for one instance ends up sent to another.
## How a check works
A check is four steps: package the tree, upload it, wait for the review, and print the verdict.
### Packaging the working tree
The archive is the working tree *as it sits on disk*. This is the central design decision, and it is not the
obvious one. A first instinct is to honour `.gitignore`, but a full legal review must see the vendored
dependencies a project actually ships or builds against - the `node_modules`, `vendor` and bundled trees that
`.gitignore` almost always hides. A CI job that runs `npm install` and then `check` is exactly the case that
matters, and dropping the installed packages would review the one part nobody wrote and skip everything the
project pulled in. So the default is: include everything, dropping only `.git` and whatever the project lists
in a `.cavilignore` file or passes with `--exclude-path`. `--respect-gitignore` is offered for the leaner case,
and because a `.gitignore` says nothing tar can be trusted to interpret, that mode honours it through git
itself rather than approximating it.
The archive is a gzip tarball built by shelling out to `tar`, chosen over an in-process library because a
vendored tree can be hundreds of megabytes and should stream to disk rather than sit in memory. Its MD5 is
computed as it is written and sent with the upload, so a truncated transfer is rejected rather than reviewed as
incomplete sources.
Before uploading, the client checks the archive against the server's upload limit (the Cavil default, or
`CAVIL_MAX_UPLOAD_MB` for an instance configured to accept more) and refuses with guidance if it is over, so an
accidental large file fails fast rather than after a slow doomed upload. The server enforces the same limit, so
a client whose limit is set too high still gets a clear rejection rather than a raw error.
### Uploading and waiting
The archive is posted to Cavil, which starts the same unpack, index and analyze pipeline any other package
goes through. Submitting the same archive under the same name again is idempotent on the server, so a re-run of
an unchanged tree does not pile up duplicate reviews.
The review is not instant, so the client polls the report endpoint, which answers "not ready" until the
analysis finishes and then returns the report. Its "not ready" reply names the pipeline stage (queued,
unpacking, indexing, analyzing, finalizing), which the client shows on the progress line so the wait names what
is happening rather than sitting on a bare "Reviewing". The line is on standard error so it never pollutes the
report or a pipe, and only on a real terminal so CI logs stay clean. A timeout bounds the wait.
### The verdict
The report carries the maximum license risk Cavil found, on its one-to-nine scale, and the instance's own
acceptable-risk threshold. The client turns those into a headline, a short tally, the licenses found ordered by
risk, and a link to the full web report, in colour on a terminal and plain text otherwise. For CI there is a
flat JSON form with the same facts.
## The gate
In CI the report is a gate: the check fails when the risk is at or above a threshold, and passes otherwise.
The threshold defaults to the instance's own `acceptable_risk` plus one, so "would Cavil consider this
acceptable" and "does the CLI pass" line up by default and no project needs to configure a number to match its
Cavil. `--fail-on-risk` overrides it for a project with a stricter or looser bar.
Gating on risk rather than on the presence of any license is the point: every project has licenses, and most
are fine. Cavil's scale captures more than copyleft (obligations, non-commercial and unknown all have their
place on it), so a single threshold expresses a real policy. The exit code is conventional: zero when within
the threshold, one when the gate fails, two for a usage or configuration problem, and three for a server or
connection error, so any non-zero code is the simple signal CI acts on while the risk itself is in the report.
## Access, and why it is high for now
Submitting a package runs the full pipeline and puts a review in the legal backlog, so it is gated exactly like
Cavil's own web upload form: it needs a read-write API key whose user has admin (`infra`) access. That is a
high bar on purpose for a first version - it keeps random submissions out of the production queue while the
workflow is proven. The planned sandbox namespace is what will open ad-hoc checks to ordinary users without
that cost; until it exists, `check` is for admins and for CI configured with an appropriately privileged key.
The client preflights with `whoami` so a key that cannot submit is reported clearly before a large tree is
packaged, rather than as an opaque rejection afterwards.
## The service contract
The CLI depends on a small, stable contract with the Cavil server, deliberately narrow so it can be reasoned
about and mocked. An identity endpoint backs `whoami`. An upload endpoint takes the archive and its checksum
and starts a review, returning the new package's id. A report endpoint returns the report for that id, or "not
ready" while it builds, with the risk and acceptable-risk fields the gate needs. A documents endpoint serves
the generated SPDX SBOM and NOTICE files, likewise "not ready" until generated. That is all; the CLI needs no
knowledge of Cavil's internals beyond these questions and their answers.
The upload also carries an `ephemeral` flag, asking for a one-off report with no lasting side effects, no open
review left in the legal backlog. That is what a developer or CI check always wants, so it is always sent. It
is the forward edge of the planned sandbox mode: today's servers do not act on it (which is why submission is
still gated to high access), but a server that gains the ad-hoc mode can honour it without any client change,
and the access gate can then relax for ephemeral requests.
## Testing
The CLI is tested against a mock of the Cavil server, not a real one. Each scenario stands up a small
in-process web service that answers those endpoints with canned data and records what the CLI asked it. The
requests go through the real HTTP machinery, so the tests exercise the true wire behaviour, but there is no
database, no network, and no port to bind. A field problem becomes a test with almost no translation: the
responses that triggered it become the mock's answers, and the requests that led to it become the assertions.
The parts that never talk to the server - the archive builder, the report renderer, the risk gate and the exit
codes - are plain functions of their inputs and tested directly, with small throwaway directories and git
repositories as fixtures.
## Design choices and their reasons
A few decisions are worth stating on their own, because they are the ones a reader is most likely to question.
- **Reuse the real review, do not reinvent it.** The value is that the local answer is the authoritative one.
A separate client-side approximation would drift from what the legal workflow decides, which is the one thing
it must not do.
- **Include vendored code by default.** Honouring `.gitignore` would be the tidy choice and the wrong one: the
installed dependencies are most of what a legal review is for. The default optimises for a correct review,
and `--respect-gitignore` is there for the rare case that wants the lean tree.
- **Gate on risk, defaulted to the instance.** A copyleft yes-or-no is too coarse, and a hardcoded number would
fight each instance's own policy. Defaulting the threshold to the instance's acceptable risk makes the common
case need no configuration at all.
- **High access first, sandbox later.** Rather than invent a weaker analysis to make submission safe for
everyone, the first version reuses the real pipeline behind the real access gate, and leaves room for a
backlog-free sandbox to open it up properly.
( run in 1.312 second using v1.01-cache-2.11-cpan-5c0b1e786e0 )