Control Requests

View as Markdown

The example’s requests group separates blocking policy from request rewriting. Its conditional guardrails read blocked_tools, blocked_models, and mode; its request intercepts read the configured header, priority, and break_chain values. Disabling the group installs none of those registrations.

Register Policy Only When Configured

The tool guardrail receives the managed tool name and real JSON request. Returning Some(reason) blocks execution; returning None continues the pipeline. observe mode uses the same configuration without blocking, which lets an operator stage a policy.

1if config.requests.enabled {
2 context.register_tool_conditional_execution_guardrail(
3 "documentation_tool_policy",
4 10,
5 {
6 let mode = config.requests.mode.clone();
7 let blocked = config.requests.blocked_tools.clone();
8 move |name, _args| {
9 let mode = mode.clone();
10 let blocked = blocked.clone();
11 async move {
12 Ok((mode == "enforce" && blocked.contains(&name))
13 .then(|| format!(
14 "tool '{name}' is blocked by documentation policy"
15 )))
16 }
17 }
18 },
19 )?;
20
21 context.register_tool_request_intercept(
22 "documentation_tool_request",
23 config.requests.priority,
24 config.requests.break_chain,
25 {
26 let tag = config.tag.clone();
27 move |name, request| {
28 let tag = tag.clone();
29 async move { Ok(tag_tool_request(request, &name, &tag)) }
30 }
31 },
32 )?;
33}

tag_tool_request returns a new object containing plugin_tag and plugin_tool; it does not mutate host-owned memory. A non-object request is returned unchanged. The priority and break_chain arguments come directly from the component configuration.

Conditional Guardrails Block Real Execution

In observe mode, the example allows configured names to execute without blocking. In enforce mode, its tool conditional guardrail rejects a matching tool and its LLM conditional guardrail rejects a matching model before the real callback runs. Validation rejects any other mode and requires string arrays for both block lists.

This is execution policy, unlike redaction. A rejected call produces the managed error path and never invokes the application callback. Tests therefore count callback invocations as well as checking the returned error.

Request Intercepts Rewrite the Real Request

The tool request intercept adds configured policy metadata to the JSON request and returns it. The LLM request intercept adds the configured header and returns the full request-intercept outcome, including the original annotated request. Preserving the annotation matters because a codec can already have normalized the request for later middleware.

1context.register_llm_conditional_execution_guardrail(
2 "documentation_llm_policy",
3 10,
4 {
5 let mode = config.requests.mode.clone();
6 let blocked = config.requests.blocked_models.clone();
7 move |request| {
8 let mode = mode.clone();
9 let blocked = blocked.clone();
10 async move {
11 let model = request.content
12 .get("model")
13 .and_then(Json::as_str)
14 .unwrap_or_default();
15 Ok((mode == "enforce"
16 && blocked.iter().any(|candidate| candidate == model))
17 .then(|| format!(
18 "model '{model}' is blocked by documentation policy"
19 )))
20 }
21 }
22 },
23)?;
24
25context.register_llm_request_intercept(
26 "documentation_llm_request",
27 config.requests.priority,
28 config.requests.break_chain,
29 {
30 let header_name = config.requests.header_name.clone();
31 let header_value = config.requests.header_value.clone();
32 let emit_pending_marks = config.execution.emit_pending_marks;
33 move |_name, mut request, annotated| {
34 let header_name = header_name.clone();
35 let header_value = header_value.clone();
36 async move {
37 request.headers.insert(
38 header_name.clone(),
39 Json::String(header_value),
40 );
41 let mut outcome = LlmRequestInterceptOutcome::new(request, annotated)
42 .with_optimization_contribution(
43 LlmOptimizationContribution::new(
44 "examples.rust_native_policy",
45 "request_rewrite",
46 ),
47 );
48 if emit_pending_marks {
49 outcome = outcome.with_pending_mark(
50 PendingMarkSpec::builder()
51 .name("example.native.llm_request")
52 .category(EventCategory::custom())
53 .data(json!({ "header": header_name }))
54 .build(),
55 );
56 }
57 Ok(outcome)
58 }
59 }
60 },
61)?;

The returned outcome keeps annotated alongside the rewritten provider request. Relay emits the optional mark and records the optimization contribution; neither appears in the provider request or provider response seen by the application.

Both request-intercept registrations take requests.priority and requests.break_chain. Priority decides where each intercept appears after global and visible scope-local entries are merged. If break_chain is true, later request intercepts do not run after this one. The example does not hide these choices in code constants: they are schema-checked configuration and appear in the validation messages.

Verify Request Policy

Use the following procedure to verify policy, rewriting, ordering, and annotation preservation independently:

  1. Activate requests.mode = "enforce" with one blocked tool, one blocked model, priority 20, and break_chain = false.
  2. Call each blocked name and confirm that its real callback count remains zero. Call an allowed name and confirm normal execution.
  3. Inspect the allowed tool request and LLM headers in the real callbacks. They must contain the configured values, proving that request intercepts affect execution.
  4. Register a later test intercept, set break_chain = true, and repeat the call. Confirm that the example rewrite runs and the later intercept does not.
  5. Repeat an LLM call with an annotated request and confirm that later request intercepts and the managed start-event path receive the annotation unchanged.
  6. Set requests.enabled = false, reactivate, and confirm that blocked names execute and no request is rewritten.

Success means configuration independently controls policy and rewriting, guardrails block before side effects, intercept ordering is reproducible, and annotations survive the complete request path.