Trust model¶
What Verel actually guarantees, how, and — just as important — what it does not. The verdict bus is a security boundary (it decides what counts as "done"), so this page states only what's implemented and names the residual risk plainly.
The unit of trust: a signed receipt¶
Every grader run produces an agentsensory.Report carrying a RunReceipt that is cryptographically
signed. The receipt binds:
inputs_digest— a hash of the exact artifact / diff the grader saw,coverage_assertion— proof the grader actually looked at the changed files (it must intersect the diff),result_digest— a hash of the graded outcome (verdict + issues),
into one signing payload (canonical, length-prefixed to prevent delimiter injection). Tamper with the report — strip an issue, swap the verdict, point it at a different diff — and the signature no longer verifies.
Algorithms. The default is attest="auto" — ed25519 when verel[attest] is installed, HMAC-SHA256 otherwise. auto never silently downgrades an explicit request: if you pass attest="ed25519" and PyNaCl is absent, the call raises MissingAttestationDep rather than falling back to HMAC (a fallback would strip the public-verifiability guarantee you asked for). Verification is always constant-time (hmac.compare_digest), and the algorithm is bound into the signed payload so a signature can't be downgraded. An ed25519 verification with PyNaCl absent fails closed — it returns false, never a silent pass.
Keys are never a hardcoded default. The HMAC secret resolves from VEREL_RUNNER_SECRET, else a
persisted per-installation random key under ~/.config/verel/, else an ephemeral per-process key (which
makes cross-process verification fail closed rather than trust a guessable default). The key file is
created atomically at mode 0600 with symlink-race and foreign-owner protection; a world/group-readable
or foreign-owned key is rejected. ed25519 seeds resolve the same way, and public keys are pinned, never
trust-on-first-use — a valid signature is necessary but not sufficient; the verifier must already trust
the key (its own runner's key, zero-config, or one you place under ~/.config/verel/trusted_keys/).
No hollow pass¶
A verdict is not trusted just because it says pass:
- a required grader that didn't run → the gate fails,
- a grader result with no signed receipt → the gate fails (you can't claim success with no evidence the check ran),
so verdict=pass, issues=[] with no receipt behind it is a fail, not a pass. This "dead-gate /
hollow-gate" rejection is what makes the whole contract meaningful.
Precise vs advisory: what can gate¶
| Class | Graders | Effect |
|---|---|---|
| Precise (deterministic evidence) | tests · types · lint · mutation · security · smell · IaC · IAM · policy · cost · KPI · telecom-config · DOM/OCR/CV · DSP/ASR | Can fail the build. |
| Advisory (open-ended judgment) | vision · LLM-as-judge · acoustic · audio-LLM | Clamped to warn — informs, never blocks. |
A low-confidence issue from any grader is also clamped to warn, so statistical insufficiency or a
hedged model call can never fail a build on its own.
Isolation for untrusted / agent-generated code¶
Agent-authored or otherwise-untrusted code is run under defence-in-depth, not in-process exec:
- Namespaces (bubblewrap
--unshare-all) — no network, no host PID/mount/IPC, a read-only view of only the system libraries the check needs, and an in-memory ephemeral/tmp. - Resource limits (
RLIMIT_*) — wall-clock CPU, address space (memory), max file size, process count. A[0]*(10**12)allocation hitsRLIMIT_AS→MemoryErrorinstead of eating the host. - Seccomp-bpf syscall filter (with
verel[container]) — deniesclone/fork,ptrace,mount, raw sockets, and similar.
Honest limits. The namespace + RLIMIT layer is mandatory for the container runner; the seccomp filter is defence-in-depth on top. If bubblewrap is absent, a generated check that needs isolation is not run (it stays unverified → fails closed), rather than silently dropping to an unsandboxed subprocess. If seccomp is required but unavailable, the runner fails closed. This is real OS isolation for a single host — it is not a substitute for a VM/microVM boundary against a kernel 0-day.
Network surfaces authenticate¶
Any Verel service that can bind a socket (the REST gate, the shared brain, the fleet control plane) follows one policy:
- Loopback (
127.0.0.1/::1/localhost) → plain HTTP, no token: zero-config for local use. - Routable (anything else) → requires both an
auth_tokenand TLS, or the server refuses to start. An empty/whitespace token counts as no token.insecure=Trueis the explicit opt-out only for a TLS-terminating proxy that already encrypts the hop.
Bearer tokens and GitHub webhook HMAC signatures are compared in constant time, shape-validated before comparison (so malformed input can't raise or short-circuit), and a client never attaches a secret to a cleartext hop toward a routable host.
Honest posture — what is NOT guaranteed¶
State only what's verified; name the rest.
- Dependencies, kernel, and OS are outside Verel's boundary — a compromised dependency or a kernel 0-day defeats the sandbox.
- DNS/egress inside the container namespace and pod-level PID caps in Kubernetes have environment-dependent residuals (bounded by the NetworkPolicy and cgroup limits you deploy).
- The IaC/IAM rule set is a denylist with a long tail by design — pair it with the external scanners
(
trivy/checkov) it drives, not instead of them. - The telecom actuator grades and classifies the declared config; it applies the operator's edit-config and binds the result to the graded intent via post-apply re-grade + confirmed-commit rollback — but the device/NETCONF backend behaviour is outside its control.
Verel never claims code is "unexploitable." It claims: every discovered vector is closed and regression-pinned, isolation and receipts fail closed, and the residual risk is the set above — which no audit eliminates. Open, unfixed attack-vector details are kept out of the public tree by policy; a fixed, regression-pinned finding is safe to describe, an open one is not.