Evidence-Gated Tailoring & the LaTeX Sandbox
The two hardest promises in the product: the AI can't invent your experience, and a resume template can't own your server.
Every AI-proposed resume bullet must carry evidenceRefs β citations into your actual profile. A bullet whose citations don't resolve is discarded before any human sees it: hallucination becomes a type error. The approved LaTeX then compiles in a sandbox: shell-escape blocked, secrets absent, network off, 15-second kill switch β because a resume template is a program, and programs can be hostile.
Resume tools have a credibility problem: ask an AI to "tailor" your resume and it will cheerfully award you three years of Kubernetes you've never touched. Wera's founding guardrail (SPEC Β§12.4) is blunt: "Do not invent experience" β and crucially, it's "structurally enforced, not prompt-only."
The mechanism is academic citation. A claim in a paper means nothing without a footnote, and a footnote pointing at a source that doesn't exist gets the paper retracted. In Wera, each bullet is a claim, its evidenceRefs are the footnotes, and your profile is the only citable literature. The review screen then shows each bullet next to its source β so approving a resume is checking receipts, not vibes.
The build journey
Make citations mandatory in the schema
The zod schema requires every bullet to have at least one evidence reference β an uncited bullet can't even parse.
resumeTailoringBulletSchema: evidenceRefs: z.array(z.string().min(1)).min(1), plus a required rationale per bullet.
The first gate is shape: a response with an uncited bullet fails validation at the AI boundary (Module 3) and triggers the corrective retry β the model is told which bullet lacked receipts.
The prompt supplies an explicit Allowed evidence: list of {id, label, detail} items flattened from the profile β the model can only cite IDs it was handed.
Resolve every citation β or reject the batch
Shape isn't truth: the model could cite an ID that doesn't exist. resolveEvidenceRefs looks up every ref and throws on the first fake.
This is the heart of the anti-hallucination gate, from packages/core/src/index.ts:
const evidence = bullet.evidenceRefs.map((ref) => {
const item = evidenceById.get(ref);
if (!item) {
throw new Error(`Unresolved evidence ref: ${ref}`);
}
return item;
});
For each citation this bullet makesβ¦
β¦look it up in the map of evidence that actually exists in your profile.
If the citation points at nothing β
β stop everything, loudly, naming the fake reference. The bullet (and the whole proposal) never reaches the review screen.
Otherwise, keep the real evidence itemβ¦
β¦so the UI can display the bullet and its receipt side by side.
Gate one (zod, Module 3) rejects bullets with no citations β a shape problem, retried with feedback. Gate two (resolveEvidenceRefs) rejects citations to nonexistent evidence β a truth problem, surfaced as errorKind: "evidence_validation" with no retry, because a model that fabricates references needs a human, not another attempt.
Escape everything, then compile in a locked room
User strings are LaTeX-escaped in a single pass; the template compiles in a throwaway directory with no secrets, no network, and a 15-second SIGKILL timer.
LaTeX is a full programming language β historically it can even run shell commands via \write18. So Wera treats every template like a stranger's code and compiles it the way you'd run a stranger's code:
compileLatexSource in packages/resume: blocklist check β per-run temp dir with fake HOME β tectonic --untrusted β typed CompileResult.
A malicious template could read environment secrets, phone home, or spin forever. Each defense kills one class: allowlisted env (no secrets exist in the child process), --untrusted + regex blocklist (no shell escape), SIGKILL timeout (no infinite loops).
buildSecretlessCompileEnv hands the subprocess exactly four variables: PATH, a fake HOME, TMPDIR, and the package cache dir. OPENAI_API_KEY isn't hidden from the compiler β it simply doesn't exist in its universe.
Bake the LaTeX universe into the deploy image
The deploy image downloads every LaTeX package at build time, then re-compiles with the network forbidden to prove nothing is missing.
A custom Trigger.dev build extension in trigger.config.ts: install pinned Tectonic 0.16.9, compile the real template to warm the cache, then compile again with --only-cached.
Without this, the first production compile downloads ~40 MB of packages inside a 15-second timeout β or worse, a missing package fails a user's compile at 6 a.m. The --only-cached re-compile turns that into a deploy-time failure instead.
Runtime sets LATEX_ONLY_CACHED=1: production compiles never touch the network at all. The comment in the code names the property: hermetic.
The central idea: make the bad thing unrepresentable
You can ask a courtroom witness to please tell the truth β or you can require every statement to come with a document the judge can hold. Wera does the second. The AI isn't trusted to be honest about your experience; it's prevented from being dishonest, because a claim without a verifiable document never enters the record. Same philosophy in the print shop downstairs: the compiler isn't trusted to behave β it's locked in a room where misbehaving is physically impossible.
Both halves of this module are the same principle: move safety properties from behavior to structure. Anti-hallucination is enforced by a data invariant (every bullet β resolvable refs) checked by pure code at a boundary β not by prompt phrasing, which is a request, not a law. Compile safety is enforced by capability removal: the subprocess can't leak secrets it doesn't have, can't reach a network that isn't there, can't outlive a SIGKILL. Defense in depth: engine flag (--untrusted) + static blocklist (\write18|\input| |openout18) + env allowlist + timeout β four independent layers, any one of which failing still leaves three.
Tradeoff and failure mode: evidence IDs are positional (projects.0 = "first item in the projects array"). They're resolved at generation time and stored denormalized, so they're sound for the version that was approved β but reorder your profile arrays later and stored refs point at different items. The stable-key migration (real profile tables, Module 2's deep dive) is the known exit.
Apply it
A malicious template tries \input{|env}-style tricks to print the server's OPENAI_API_KEY into the PDF. What actually happens?
The model returns 5 bullets; 4 cite real profile items, 1 cites projects.7 β which doesn't exist. What reaches the review screen?
A teammate adds \usepackage{fontawesome5} to the resume template but forgets everything else. Where does this surface first?
Sandboxing & structural guarantees
Two concepts to keep forever. Capability removal: the strongest sandbox isn't one that detects attacks, it's one where the attack's target doesn't exist (no secrets in env, no network in the runtime). Grounded generation: when AI output must be true, require it to cite sources you control and verify the citations mechanically β the pattern behind serious RAG systems, and here, behind a resume tool you can actually trust.
Best practices
- Allowlist subprocess environments β never inherit
process.envinto anything that runs user input. - Layer independent defenses (flag + blocklist + env + timeout); assume any one layer fails.
- Verify AI citations against a closed set of IDs you issued β and reject, don't repair, fabrications.
- Prove hermeticity at build time (
--only-cached) so missing dependencies fail deploys, not users.
Common pitfalls
- Prompt-only guardrails ("do not invent experience") with no structural check β a request, not a law.
- Escaping user strings after assembling the document β order matters; escape at the data boundary.
- Positional IDs as long-lived references β fine denormalized-at-approval, dangerous as live pointers.
- Timeouts without SIGKILL β a polite signal to a hung process is a suggestion.
The engine Wera pins β read about --untrusted and the bundle/cache model behind hermetic compiles.
Why a typesetting language needs a sandbox at all β the attack class this module defends against, from the TeX community itself.
β Docs Trigger.dev documentationtrigger.devTasks, schedules, and build extensions β the machinery that runs the compile off the request path and bakes the cache.
β