Crux
GuidesSafety

Safety

Boundary-targeted guardrails, constraints, tool policies, validation retry, and prompt-injection-resistant patterns.

Four incidents, four different fixes:

  • The model echoed a customer's email address into your logs. That is a guardrail: filter, transform, redact, warn, or block on typed boundaries.
  • The output parsed cleanly but cited no sources. That is a constraint: validate output semantics and retry the model with feedback on business-rule violations.
  • A tool call tried to delete a production record. That is a tool policy: block, request approval, or screen tool calls/results through the Safety decision model.
  • The model returned broken JSON. That is validation retry: Crux text-repairs first, then re-prompts with the Zod error.

Separate concern: Security, patterns to make your prompts injection-resistant.

Boundary targets

Guardrails and constraints bind to boundary.* descriptors instead of a broad "input/output" phase:

import { boundary, constraint, guardrail } from "@use-crux/core/safety";

export const redactPii = guardrail({
  id: "pii",
  on: [boundary.input.text(), boundary.output.text()],
  run: guardrail.pii({ strategy: "mask" }),
});

export const grounded = constraint({
  id: "grounded-answer",
  on: boundary.output.object<{ answer: string; citations: string[] }>(),
  run: constraint.judge({ judge, minScore: 0.8 }),
});

Useful targets include boundary.input.text(), boundary.input.media(), boundary.input.instructions(), boundary.input.tools(), boundary.output.text(), boundary.output.media(), boundary.output.object<T>(), boundary.output.object<T>().path('a.b'), boundary.memory.write<T>(), and boundary.input.text({ from: "feedback" }). The optionless input helpers cover all supported sources. Advanced policies can filter with boundary.input.text({ from: "retrieval" }) or boundary.input.media({ from: "tool" }). Use toolPolicy.args() and toolPolicy.result() for raw tool lifecycle decisions.

Think of an input boundary as destination plus provenance. The destination states what the provider will receive; from states who supplied that canonical content:

const lifecycleText = boundary.input.text({
  from: ["memory", "handoff", "feedback"],
});
const discoveredTools = boundary.input.tools({ from: "discovered" });

Rendered managed memory and blackboard state both use source memory, with distinct ctx.origin.kind values. Handoff context uses handoff; validation and constraint retry writeback uses feedback. Provenance is safe metadata, not the guarded text or payload.

See Safety for content primitives for the exact language, structured, stream, image, speech, and transcription coverage matrix.

For an application-oriented walkthrough of input media, generated previews, held deltas, final assets, and required-media strip behavior, see Media safety.

Report mode and streaming

Use safety.tune at a call site to shadow a policy or disable it:

await adapter.generate(prompt, {
  input,
  safety: {
    tune: {
      pii: { mode: "report" },
      "grounded-answer": { enabled: true },
    },
  },
});

Tune changes only mode and enabled. The streaming unit is not tunable: it is part of the policy's boundary, so a call site cannot widen what a policy sees. In report mode, input envelopes, exposed tools, generated candidates, retry feedback, and memory writes remain unchanged; Crux records what the policy would have done.

Built-in text guardrails such as guardrail.pii(), guardrail.secrets(), and guardrail.injection() check sentences by default; guardrail.classifier() checks the complete subject. Refine the boundary — .sentences(), .lines(), .deltas(), .complete() — to override that.

Guardrails and constraints do different things on a stream. A guardrail is a pass over content: it holds a unit, clears or rewrites it, and releases it — it never re-calls the model. An assert constraint is transactional: while it is unresolved the stream withholds output, and if it fails the whole attempt is discarded — the consumer saw none of it — and Crux re-streams with corrective feedback. That is why a guardrail can act per sentence while a constraint acts per attempt.

The boundary, plainly

A recap of the four primitives side by side:

GuardrailConstraintTool policyValidation retry
Runs onSemantic model input/output, memory, and validation feedbackOne output boundaryRaw tool call, result, or approval lifecycleStructured output parse/validation
What it catchesForbidden content, PII, secrets, injection, formattingSemantic / business-rule violationsUnsafe tool execution or unsafe tool I/OSchema-parse failures, wrong types
What happens on a hitBlock, rewrite, warn, or reportRe-call the model with custom feedbackBlock, request approval, report, or rewrite tool I/OText-repair first, then re-call with the Zod error
Knows about retriesNo: it is a policy passYes: retry is the purposeNo, except approval suspensionYes: bounded by maxRetries and maxSteps
You writeguardrail({ id, on, run })constraint({ id, on, run })toolPolicy({ id, match, action }) or toolPolicy.args/resultvalidationRetry: { maxRetries }

Rules of thumb:

  • Schema parse failed? That's validation retry: built-in, just enable it.
  • Output parsed but business rule failed? That's a constraint: write a check.
  • Need to filter, redact, or transform content? That's a guardrail.
  • Need to gate tool execution? That's a tool policy. Use tool middleware for execution plumbing.

When should I use a guardrail?

  • You need to redact PII before logging, before sending to a tool, or before showing output
  • You want to block certain inputs entirely (profanity, prompt-injection markers)
  • You want to transform outputs (strip leading code-fence markers, normalize whitespace, decode entities)
  • You want to warn without blocking (attach metadata for downstream code)

When should I use a constraint?

  • The model occasionally returns output that fails a business rule (off-topic, wrong language, missing field)
  • You're willing to spend an extra round-trip to get correct output
  • Your output schema validates most outputs but a small percentage need a re-prompt with the validation error

When should I use neither?

  • The output is structurally invalid (won't parse): Zod schema validation already handles that
  • The check needs to read external state (database, RAG): use a tool call, not a guardrail
  • The check is about who is asking, not what: that's auth, not safety

Inspect and test safety

Safety decisions emit observability artifacts, Turn Decision Report rows, and Project Index definitions. Devtools opens runtime interventions in Runs Explain with model, action, semantic target, source (User, Tool, Retrieval, Memory, Blackboard, Handoff, Feedback, Authored tool, or Discovered tool), privacy-safe origin coordinates, and strip escalation. Catalog shows authored canonical boundary tuples, strategy helper configuration and action, links from policies to completed operations, operation-side Safety attachments, and duplicate policy-id lints. Source filters affect runtime matching but are not stored as Project Index facts.

Safety sessions currently govern the application-facing model call. Internal judge, summarizer, reranker, and classifier model calls are not automatically wrapped by the originating session; configure their own Safety policy when their implementation exposes one.

In Evals, use safety signal assertions for guardrail/constraint audits or the decision report matcher for the canonical safety read model:

ctx.expect.safety.toHaveBlocked("pii");
ctx.expect.decisionReport.safety.toHaveOutcome("pii", "rewrite", {
  reasonCode: "guardrail.redacted",
});

Pick a topic

How this fits with the rest of Crux

  • Eval validates output quality during testing. Guardrails and constraints validate it during execution. constraint.judge(...) bridges a scoring judge into runtime Safety enforcement; Eval safety and decision-report matchers regression-test the resulting production behavior.
  • Tools can implement domain-specific safety checks that need external data.
  • Devtools trace every guardrail hit and constraint retry.

On this page