Configure a guardrail
This guide gives a guardrail's check its settings, so you change a limit without touching the check's code.
Before you start
Section titled “Before you start”- The answer-length guardrail from
Write a guardrail, in the pack
my-pack, withkindgi devrunning and Kindgi 0.1's CLI.
Declare the settings
Section titled “Declare the settings”A check's settings don't belong in its code: a limit, a list of words or a
tool's id, for example. The guardrail declares them as config, and the
check runs with them. Ask your coding agent, for example: "I want to change
my guardrail's limit without touching its check." Kindgi's skills in your
pack tell it how (How it knows Kindgi).
For reference, the answer-length guardrail has one setting, maxChars:
// in guardrails/answer-length/index.tsexport const check = defineCheck({ id: 'my-pack.checks.answer-length', kind: 'zero-llm', configSchema: z.object({ maxChars: z.number().int().positive().default(500) }), evaluate: async (config, trace) => { // config.maxChars: 60 here, or 500 when the declaration gives none // … },});
export default { id: 'my-pack.answer-length', // … check, config: { maxChars: 60 }, // …};configSchema describes the settings. config in the declaration gives
their values. The check's evaluate gets those values, typed from the
schema.
# in guardrails/answer_length.pyclass Config(BaseModel): max_chars: int = Field(500, alias="maxChars", gt=0)
@guardrail( id="my-pack.answer-length", # … config={"maxChars": 60},)def answer_length(config: Config, trace: RunTrace) -> CheckResult: ...The type of the check's first parameter describes the settings. config=
gives their values, keyed by their wire names, which are the aliases:
maxChars, not max_chars.
// in src/main/java/mypack/guardrails/AnswerLength.java public record Config(@JsonProperty(defaultValue = "500") @Positive int maxChars) {}
public static final Guardrail<Config> GUARDRAIL = Guardrail.define("my-pack.answer-length") // … .config(Config.class) .set("config", Map.of("maxChars", 60)) .check((config, trace) -> { // config.maxChars(): 60 here, or 500 when the declaration gives none // … });The Config record describes the settings: its fields' types and
annotations make the schema, and @JsonProperty(defaultValue = …) a
default. set("config", …) gives their values, keyed by their wire names.
The check gets those values as a Config.
// in src/main/scala/mypack/guardrails/AnswerLength.scala final case class Config(@Positive maxChars: Int = 500)
val guardrail: Guardrail[Config] = Guardrail[Config]("my-pack.answer-length") // … .set("config", Map("maxChars" -> 60)) .check { (config, trace) => // config.maxChars: 60 here, or 500 when the declaration gives none // … }The case class describes the settings: a field's default value is the
schema's default. set("config", …) gives their values, keyed by their wire
names. The check gets those values as a Config.
Change a setting
Section titled “Change a setting”A new value takes effect without a restart. Edit the value and save. The
next turn uses it. With maxChars: 300, the dev-echo answer from
Write a guardrail passes:
pnpm exec kindgi runs start --agent=my-pack.echo-agent --input='{"userMessage":"hi"}'uv run kindgi runs start --agent=my-pack.echo-agent --input='{"userMessage":"hi"}'./kindgiw runs start --agent=my-pack.echo-agent --input='{"userMessage":"hi"}'./kindgiw runs start --agent=my-pack.echo-agent --input='{"userMessage":"hi"}'--agent is the agent the guardrail is on, and --input its input.
"status": "completed", … "violations": [],The registered guardrail shows the values it runs with. Get it by its id:
pnpm exec kindgi guardrails get my-pack.answer-lengthuv run kindgi guardrails get my-pack.answer-length./kindgiw guardrails get my-pack.answer-length./kindgiw guardrails get my-pack.answer-length "config": { "maxChars": 300 },What the check receives
Section titled “What the check receives”The check gets config resolved by its schema:
- Defaults are filled in. A guardrail without
configruns with the schema's defaults; here, a limit of 500. - A value that doesn't fit is refused, and the message names the setting. In TypeScript and Python that happens before any turn runs; in Java and Scala, on the turns the guardrail checks.
Zod's .default() and a JSON Schema's default both apply. When the pack is
indexed, each guardrail's config is checked against its configSchema, so
kindgi dev reports the file and kindgi build refuses the pack. With
config: { maxChars: -5 }:
guardrails/answer-length/index.ts: guardrail my-pack.answer-length's config doesn't fit its configSchema at /maxChars: must be > 0A setting the schema requires, with no default, must be in config:
guardrails/answer-length/index.ts: guardrail my-pack.answer-length's config doesn't fit its configSchema: must have required property 'maxChars'In a deployment, a guardrail registered through the API, with
kindgi guardrails register, names the pack's check with its own config.
A config that doesn't fit the check's configSchema is refused with
422 guardrail-config-invalid, and nothing is registered:
Error [invalid-request]: Guardrail "acme.strict-length" doesn't fit check "my-pack.checks.answer-length": config.maxChars must be > 0. ✗ /config/maxChars: config.maxChars must be > 0.Under kindgi dev the indexer has refused such a config already.
The pack service also checks the config on every call, as sent, against the
indexed configSchema. A registered guardrail whose config doesn't fit
fails every turn it checks, whatever its action: the check doesn't run.
kindgi runs start exits 1 with:
Error [run-failed]: input-validation-failed: Check "my-pack.checks.answer-length" config failed validation at /maxChars: must be > 0To fix it, unregister the guardrail and register it again with a config that fits.
The check gets config parsed into its type:
-
Defaults are filled in. Give every field a default: a required field with no value isn't caught when the pack is indexed, and fails every turn the guardrail checks, whatever its action.
kindgi runs startexits 1 withError [run-failed]: input-validation-failed: Check "my-pack.req-cfg" config failed validation: must have required property 'maxChars'. -
Values are checked where they're declared. A value that doesn't fit is a
DefinitionError.kindgi devreports the file, and so doespython -m kindgi.pack index, which indexes the pack:Guardrail "my-pack.answer-length": config does not fit its type: 1 validation error for ConfigmaxCharsInput should be greater than 0 [type=greater_than, input_value=-5, input_type=int] -
A key that isn't a wire name is ignored.
config={"max_chars": 60}isn't an error: the field keeps its default.
The check gets config as a Config:
-
Defaults are filled in. A guardrail without
set("config", …)runs with the schema's defaults; here, a limit of 500. -
A value that doesn't fit isn't caught before a turn. Indexing the pack and
kindgi devacceptMap.of("maxChars", -5). The pack service checks the config on every call, so the check can't run: withhalt, every turn the guardrail checks fails, and./kindgiw runs startexits 1 with:Error [guardrail-violation]: Turn blocked: guardrail 'my-pack.answer-length' couldn't run its check (check-failed: guardrail "my-pack.answer-length": its check threw: input-validation-failed: Check "my-pack.checks.answer-length" config failed validation at /maxChars: must be > 0)With another action, the turn completes, and the error is recorded as When the check can't run describes. Fix the value and save.
-
set("config", …)is keyed by wire names, as the schema names them:maxChars.
The check gets config as a Config:
-
Defaults are filled in from the case class's default values: without
set("config", …), a limit of 500. -
A value that doesn't fit isn't caught before a turn. Indexing and
kindgi devacceptMap("maxChars" -> -5). The pack service checks the config on every call, so the check can't run: withhalt, every turn the guardrail checks fails, and./kindgiw runs startexits 1 with:Error [guardrail-violation]: Turn blocked: guardrail 'my-pack.answer-length' couldn't run its check (check-failed: guardrail "my-pack.answer-length": its check threw: input-validation-failed: Check "my-pack.checks.answer-length" config failed validation at /maxChars: must be > 0)With another action, the turn completes, and the error is recorded as When the check can't run describes. Fix the value and save.
-
set("config", …)is keyed by wire names, as the schema names them.