Redouble AI Nucleo documentation
Part 13 · Inside the runtime

Inside guardrail enforcement

ai.redouble.nucleo.guardrails · nucleo-core

This page is for people working on the runtime itself: the plumbing every guardrail inherits, the declaration interfaces, how each scope value judges, and the rules of the doors that enforce them. The guide for writing guardrails is Writing guardrails, and the concepts behind them are in Scope and the trust boundary. The enforcement engine (GuardrailEnforcer) and the submission door (JobDispatcher) live in ai.redouble.nucleo.harness.

The sealed hierarchy

Guardrail is sealed to exactly four kinds - one per rung of the context ladder, each consuming one more piece of framework context. Scope is deliberately NOT a guardrail kind: a scope judgment happens at the submission door in JobDispatcher, against a value the flow carries, with no job spawned - see the scope vocabulary below.

Common contract on Guardrail: validate(T) throwing GuardrailException (always correctable), setTarget, targetType() (self-reported applicability class - the framework never resolves it reflectively), setGatedSnapshot. No live framework object ever crosses into a guardrail. A refusal speaks to the model as Guardrail violation: followed by the message the guard threw, and keeps any cause it was given (GuardrailContractTest).

AbstractGuardrail is branch-free job plumbing (target/snapshot fields, resource capture, observability metadata, final execute-calls-validate) plus the scope-guard seat a guard needs only when it declares ScopeAuthority (see below). The per-branch bases - AbstractContentGuardrail, AbstractAuthGuardrail, AbstractAdmissionGuardrail, AbstractValidationGuardrail - add their branch interface and the gates_phase rung label (AUTH / ADMISSION / CONTENT_INPUT / CONTENT_OUTPUT / VALIDATION).

The plumbing, pinned by GuardrailContractTest: a guardrail is a job of type GUARDRAIL with a read-only requirement and a ten-second timeout unless its author sets another; execute is final and calls validate on the target set before submission, returning nothing; before it runs it records the target's serialized form under obs.input (an admission guard records principal via caller -> Tool there instead), the gated job's id under obs.gates_job_id and the rung under obs.gates_phase, and after it runs obs.output is FAIL when validate threw and PASS otherwise, the guard's own record since the context never carries the failure; the gated snapshot, and so an auth guard's principal, is null when the guard runs outside dispatch enforcement (the Prompts facade gates no job); setScopeGuard refuses with UnsupportedOperationException while sealScopeGuard installs what the door composed. An admission guard validates a Void target, so validate is checkAdmission; its AdmissionContext carries a null callerClassName for a call directly under the workflow root. GuardedExecution declares no guardrails of any kind by default.

Declaration interfaces

The scope vocabulary

TenantScope + TenantScoped are the reference scope axis - the complete recipe in two files: a record implementing Scope, a marker extending Scoped with the id getter and a default scope() that produces the record from the getter.

The submission door

Enforcement is the submission door in JobDispatcher.dispatch: only orchestrators may submit from inside a job; the caller's sealed guard judges the child's own scope and the child input's claim; descendants inherit the guard by merge. A refusal is a born-failed handle whose get() throws ExecutionException caused by the GuardrailException - the same surface as every other guardrail refusal. The door's other rules are refusals as SystemException, never scope verdicts: a plain job or a thread spawned inside execute() may not submit; a submission must arrive on the thread of its live parent, so nothing from outside can inject a child under a running job; an orchestrator whose scope() throws is a refused submission carrying that failure, never an unscoped escape; a delayed submission fires on a fresh thread and so creates workflow roots only. Concurrent workflows keep their guards apart, and a caller outside any job submits and awaits freely (GuardrailEnforcementTest).

The job-based kinds are enforced by GuardrailEnforcer on the dispatch path, the one place every route crosses - thinker-driven, doer-direct, fan-out worker, delegation - so protection is the tool's, never the route's: admission, INPUT content and auth guards run after dependency resolution and before resources are allocated, OUTPUT content guards after resources are released and before the result is delivered. A guard failing with anything but GuardrailException fails the gated job closed with that failure as itself, never laundered into a refusal. Guards run once per submission, never again on a transparent retry, and the applied counts are written to obs.guards_input, obs.guards_output and obs.guards_validation, so a zero-guard execution is an auditable fact. Delegation copies the spawner's tools as well as its guard: a delegate offers an inherited mutating tool only when no read-only binding travels with it, and both bindings survive a second hop. Prompt resolution from a job thread passes the door. At the validation seat of a SingleObjectiveThinker a refusal costs the thinker one iteration and the corrected candidate is delivered; refusals that exhaust the iteration budget end in a null answer, as when no answer was reached at all. An AbstractToollessThinker allows MAX_GUARDRAIL_CORRECTIONS correction turns and then fails with the refusal.

Guard jobs cross the same scope wall at the internal door, which skips only the authority and parent walls (a guard is submitted on a gated job's behalf, and the gated job may be a leaf). The caller guard there is the guard the gated job was admitted under: an orchestrator's own sealed guard, or a leaf's live parent's - the parent is the submitter by the door's own rule, and its sealed guard is what the leaf's claim was judged against. A refusal at the internal door throws to the enforcer rather than minting a born-failed handle, and the enforcer reports it as a SystemException of the gated job: a guard that never ran rendered no verdict, and a guard whose own claim disagrees with the flow it guards is a code error, not a policy decision.

Reference guardrails

One per rung, genuinely usable, doubling as the enforcement-test fixtures (GuardrailEnforcementTest):

ClassRungDemonstratesRefuses
PayloadSizeCapGuardrail(parent, maxChars)content INPUTparameterized instance declarationan input whose serialized JSON is longer than the cap, naming the length, the cap and the gated job's class (this tool outside enforcement); a null input passes
PIIDetectionGuardrailcontent OUTPUTscreening a result's serialized forma result whose serialized JSON matches a Social Security number, a credit card number, an email address or a phone number, naming every kind it found in one refusal and logging that summary at ERROR, never the content; a null result passes
PrincipalAllowListGuardrail(parent, principals)authjudging snapshot.getUserId()a principal outside the set, a missing principal included, naming the principal, the gated job's class and the allowed principals
AgentClassAllowListAdmissionGuardrail(parent, classes)admissionthe AdmissionContext identity payloada caller class outside the set, naming the tool, the caller (a direct call when there is no agent class) and the allowed agents. A missing context is a code error, thrown as UncorrectableRuntimeLLMException: the enforcer and ToolHub always deliver one, so a null one means init was never called

Their own logic is pinned in SampleGuardrailsTest; both content guards target Object, so they apply to any input or result. The enforcer never hands a guard a null target: a guard applies only to a target that is non-null and an instance of its targetType(), so the null cases above matter only to a caller running a guard by hand.

The validation rung ships no reference guard: what a valid answer is belongs to the producer's domain. Its enforcement fixtures live with the test (RefusingValidationGuard, CrashingValidationGuard, TenantClaimingValidationGuard).

^