carlos / platform Public

docs: carlos store scan, and what a clean verdict does not mean

The whole scan family under the store section, plus the three console
environment variables.

The part that matters most to an app author reading only this page: a
scanner cannot read ciphertext. Client-side encryption is the default
posture across this platform family, so someone could reasonably turn
scanning on, see it succeed, and believe their objects had been
examined. The docs say plainly that a clean result on an encrypted
store means nothing, and that the attestation exists because neither
answer is safe to guess.

Also documented: the coverage line names two numbers and they are not
complements; enabled is not enforcing.

Deviation from the plan, recorded here rather than silently: the plan
said to add the environment variables to README.md too. README carries
no store environment variables at all — no store section — so a
scanning block there would be the only store content in the file. They
are documented in the CLI reference beside CARLOS_STORE_OPERATORS,
which is where the store's other operator variable already lives.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJkJkxZZLgW8X8A5tk69Sq
Paul Campbell pushed by paul@keymail.dev b93cfe1fdd09086f688049980a6700e21d0141ad parent ef22cfe
1 file changed, +62 −0
  • docs/site/cli.md +62 −0
diff --git a/docs/site/cli.md b/docs/site/cli.md
index 22561c166..d38f49c5d 100644
--- a/docs/site/cli.md
+++ b/docs/site/cli.md
@@ -510,6 +510,68 @@ deployment's own cloud account, so it also wants an address on
stranger still meets the same 404 as everyone else rather than a 403 that
would confirm the app exists.
+#### carlos store scan
+
+Virus scanning over the objects in an app's store. **Off by default and
+opt-in**: a deployment that sets nothing has no engine, runs no sweep, and
+these verbs refuse with a sentence naming the variable that would wire one up.
+
+```sh
+carlos store scan request --app hello --plaintext
+carlos store scan status --app hello
+```
+
+`request` is the member's verb, and it takes an attestation you cannot skip:
+either `--plaintext` or `--client-encrypted`. **A scanner cannot read
+ciphertext.** If your app encrypts objects before uploading them — which is
+the default posture across this family of platforms — then no engine can say
+anything about their contents, and a result claiming they are clean would be a
+true-looking sentence about bytes nobody examined. Attesting
+`--client-encrypted` records that honestly: the store is marked not
+applicable, no sweep runs, and `status` says so instead of showing coverage.
+There is no default answer because neither one is safe to guess.
+
+The remaining verbs belong to a deployment operator on
+`CARLOS_STORE_OPERATORS`, the same list `carlos store grant` uses:
+
+| verb | what it does |
+| --- | --- |
+| `grant --app <a>` | turn the engine on for this store |
+| `revoke --app <a>` | turn it off, and clear enforcement |
+| `sweep --app <a>` | run one budget of scanning now, instead of waiting for the hourly pass |
+| `enforce --app <a> on` | deny the app's own credentials read access to any object the engine tagged infected |
+| `enforce --app <a> off` | lift that denial |
+
+`revoke` and `enforce off` work even when no engine is configured. That is
+deliberate: unsetting the engine on a deployment with an enforcing store would
+otherwise leave the denial in place with no way to clear it.
+
+**What `status` will and will not claim.** The coverage line always names two
+numbers — how many objects were scanned, and how many were not. They are not
+complements. An object too large to stream past the engine was never judged at
+all, and a bucket full of them could report zero infections while nothing had
+actually been looked at. A sweep still in progress says so and says which
+numbers are stale; a first sweep that has not finished claims no coverage at
+all. Signature-database age is printed, and called out when it is old, because
+a clean verdict from three-month-old definitions is a weaker claim than a
+fresh one.
+
+**Enabled is not enforcing.** With scanning on but enforcement off, infected
+objects are recorded and still readable. `carlos store status` says which of
+the two you have.
+
+The operator side is three environment variables on the console:
+
+| variable | default | meaning |
+| --- | --- | --- |
+| `CARLOS_STORE_SCANNER` | unset | `clamav` is the only engine this build accepts. Unset means no scanning at all. |
+| `CARLOS_CLAMD_SOCKET` | `/run/clamav/clamd.ctl` | the unix socket to reach `clamd` on |
+| `CARLOS_CLAMD_MAX_BYTES` | 25 MB | objects larger than this are recorded as skipped without being fetched; mirror `clamd`'s own `StreamMaxLength` |
+
+Setting `CARLOS_STORE_SCANNER` without `CARLOS_STORE_BACKEND` is refused at
+boot — a scanner with no object stores to scan is a misconfiguration, and one
+loud failure beats a pass that silently finds nothing forever.
+
### carlos email
Tenant email sending. `carlos email enable` is the whole path in one