Xavier Evidence Guard

An agentic merge-request gate that refuses to ship a carbon or security claim it cannot prove.


Inspiration

Two things kept bothering me.

The first is how cheap optimization advice has become. A model will hand you a greener configuration in seconds: buffer your writes, tighten the container, drop a syscall. The second is how expensive proof still is. A benchmark table in a README can be copied from another machine, rounded until it looks good, or quietly left behind while the code moves on. By the time it reaches a merge request, nobody can tell whether the number describes this code or a version from six months ago.

The second thing was that "this is 50% greener" and "this is safe" are the same epistemic problem. Both are claims about runtime behaviour. Both have to be observed, recorded, and re-checked against the code that produced them. Security teams solved part of this with reproducible builds and policy gates. Carbon claims mostly have not, they live in slide decks and blog posts, detached from the commit.

So the question behind Xavier was never "can an agent propose an optimization?" It was: can an agent be trusted to touch a sustainability claim at all? My answer was to make the evidence the gate, and to give the agent just enough permission to explain and repair it — never enough to invent it.

What it does

Xavier runs a workload under observation and produces four artifacts that must agree with each other:

  1. seccomp_profile.json — a default-deny allowlist generated from the syscalls the workload actually used.
  2. measurements/baseline.json and measurements/optimized.json — raw run records.
  3. sci_report.md — the report derived from both records.

The loop is: trace → allowlist → enforce → measure → gate.

A deterministic validator checks that the allowlist is exactly the traced minimum, that the report's arithmetic matches its own inputs, that the source fingerprint matches the checkout, and that the sandbox still serves traffic while blocking the escape syscall. A GitLab Duo flow then audits the bundle on the merge request, explains any failure, and proposes the smallest fix that does not require new measurements — behind two explicit human approvals.

How I built it

Observation. A small eBPF program (bpf/syscall_tracer.c, loaded with cilium/ebpf) attaches to the tracepoint set and records syscall counts for a process from execve onward. The tracer emits both a run record and a candidate seccomp profile from the same observation, so the profile is derived, not hand-written.

Enforcement. The profile denies by default (SCMP_ACT_ERRNO) and allows only the traced set. The workload is then re-run under the profile, and the gate proves GET /healthz and GET / return 200 while GET /danger — which calls memfd_create — returns 403.

Measurement. Energy comes from the host's Intel RAPL package counter over the whole process lifetime, including startup and teardown. SCI is computed per request:

$$ \mathrm{SCI} = \frac{E \times I + M}{R} $$

where $E$ is measured energy in kWh, $I$ is grid intensity in gCO₂e/kWh, $M$ is embodied carbon, and $R$ is the functional unit — one request. The functional unit matters: the comparison is per request, not per second, so a faster machine cannot win by doing less work.

The result on 1,000 requests, same host, same payload:

Metric Per-byte baseline Buffered
Syscall calls 4,198,105 53,633
write() calls 4,097,929 2,966
Energy (kWh) $4.100156\times10^{-5}$ $1.9704398\times10^{-5}$
SCI (gCO₂e/request) $1.9475741\times10^{-5}$ $9.3595888\times10^{-6}$

$$ \Delta \mathrm{SCI} = \frac{1.9475741\times10^{-5} - 9.3595888\times10^{-6}} {1.9475741\times10^{-5}} \times 100\% = 51.94\% $$

The baseline is deliberately pathological — it writes the response one byte at a time, so it performs one write(2) per byte. That is the point: the product is the loop that makes a claim checkable, not the size of the number.

Gating. A Python validator is the merge gate. It cross-checks the allowlist against the optimized run, the report against both runs, the fingerprint against the checkout, and the arithmetic against itself. CI runs it in a twelve-stage pipeline that also builds, vets, race-tests, scans, packages an SBOM, publishes a Pages dashboard, serves Prometheus metrics, and enforces a per-request carbon budget.

The agent. The GitLab Duo Agent Platform flow has five components:

evidence_auditor  (read-only)
  → human_approval          gate 1: approve the diagnosis
  → remediation_planner     (read-only; exact change set)
  → plan_approval           gate 2: approve the exact diff
  → remediation_executor    (only writer; cannot merge)

The auditor reads MR content with a least-privilege toolset and runs the audit. The planner turns the approved diagnosis into an exact change set: file, line, old text, new text, verification command, commit message. The executor is the only component with write tools, it has no raw-merge-request tools, and its only inbound route is the second approval gate. A CI job, assisted_policy, fails the pipeline if a future edit removes a gate, gives the planner write tools, or makes the executor reachable any other way.

What I learned

Deterministic gates should own truth; models should own explanation. The most important design decision was refusing to let the agent be the source of a verdict. The validator finds the defect. The agent's value is that it can read the failure, explain why it matters, and propose the smallest honest repair — work that is genuinely hard to script, but only safe when the verdict is not up for negotiation.

"Assisted" is a permissions problem, not a prompt. You cannot ask a model to wait for a human and call that a gate. The gate has to be structural: a separate component, a router condition, and an executor that is statically unreachable without an approval value.

Two gates is not bureaucracy. The first gate approves the diagnosis; the second approves the exact diff. They catch different mistakes. Approving "yes, ptrace is unnecessary" is not the same as approving "delete this one line and nothing else."

The environment is part of the claim. A green pipeline that did not actually run the check is worse than a red one, because it teaches people to trust the wrong signal.

Scope is easy to claim and hard to earn. Touching nine DevSecOps stages takes a pipeline. Making each stage carry real evidence takes judgment about which stages deserve depth.

Challenges

The flow schema said no. Custom flows rejected several fields I expected to use — top-level name/description, per-component max_cycles, require_tool_approval, and pre_approved_tools. I had to design the assisted boundary out of the primitives that actually exist: component isolation, routers, and a second HumanInputComponent over the exact change set. The constraint made the design better, because the boundary became visible in the graph instead of hidden in a flag.

The CI live check could not run and I had to say so. The committed profile is generated on the evidence host, where the Go runtime uses clone3. GitLab's shared Docker runner returns ENOSYS for clone3 in its outer seccomp profile, so the runtime falls back to clone, which the traced minimum does not allow, and the sandboxed workload cannot start. The tempting fix was to widen the profile until CI went green. That would have destroyed the entire claim. Instead, the audit takes --no-enforcement with an explicit reason and records enforcement: skipped, while the passing 200/200/403 run stays committed in sci_report.md and is reproducible on the evidence host. A recorded skip is honest; a green lie is not.

The agent's best work left an empty diff. After the flow removed ptrace, the merge request had nothing left to show. The Changes tab was empty, which looks like nothing happened. The real evidence is the commit history — the defect commit, the failed pipeline, and xavier-guard's one-line fix plus the pipeline that went green. Learning to present the process rather than the product was a design lesson as much as a storytelling one.

Refusing to manufacture a number. When a finding is measurement-bearing — a stale fingerprint, a changed source tree the correct fix is to re-run on a privileged host with tracefs and a readable RAPL counter. The planner cannot do that, and it must not pretend to. So it refuses to edit measurements/*.json or sci_report.md, and the executor opens a checklist issue instead. Designing the refusal path was harder than designing the fix path, and it is the part that makes the agent trustworthy.

Measurement honesty. RAPL measures the CPU package, not one process. Grid intensity is an input assumption, not a measurement. Embodied carbon is zero by default. The baseline is synthetic and pathological. Every one of those boundaries is written down in the repository, because a sustainability claim that hides its assumptions is just marketing with a decimal point.

What's next

Run the loop against a real service rather than a synthetic workload, move from package-level RAPL to cgroup-scoped energy where the kernel exposes it, support multiple grid-intensity inputs instead of one assumption, and add an MCP client so the flow can pull regional carbon data instead of reading a local file. The invariant stays the same: the agent may explain and repair, but it may not invent the number, and a human merges.

Built With

Share this project:

Updates

Submission history