Skip to content

Core Concepts

Labels

A label is a pair of tag sets attached to a piece of information.

Label(source={"user"}, visibility={"user", "support"})

source — where the information came from. Used for integrity checks. A tag in source means: this information was produced by that origin and can be trusted to control tools that require it.

visibility — where the information may flow. Used for confidentiality checks. A tag in visibility means: this information may be shown to or influence output for that audience.

These two axes are independent. A trusted source does not imply broad visibility. Broad visibility does not imply a trusted source.

Tag sets

Tags are plain Python strings. Tag sets are plain Python sets. There is no built-in hierarchy, alias, or fuzzy matching — "system" is not related to "internal", and "customer:A" is not related to "user:alice" unless you explicitly put the same tag in both places.

# These are unrelated
Label(source={"system"}, ...)
Label(source={"internal"}, ...)

# These match because they share a tag
Label(source={"support-agent"}, ...)
strahl.tool(requires=Label(source={"support-agent"}, ...), produces=...)

Two sentinels are provided:

from strahl import ALL, NONE

ALL   # frozenset({"*"}) — matches any source or visibility
NONE  # frozenset()      — matches nothing

Use ALL when any source may control a low-risk tool. Use NONE when a tool should never be reachable from the current context.

Dynamic labels

Labels can be static (resolved at registration time) or dynamic (resolved from tool call arguments at analysis time).

# Static — the tag set is fixed
Label(source={"user"}, visibility={"user"})

# Dynamic — the tag set depends on a tool argument
Label(source={"support-agent"}, visibility=lambda customer_id: {f"customer:{customer_id}"})

Dynamic labels are useful when a tool's security boundary is not known until the model selects arguments. The SDK resolves callable label fields from the actual arguments before sending the analysis request.

The parameter names in the lambda must match parameter names declared on the tool. Registration fails if they do not.

Tool flow policies

A tool flow policy declares what is required to control a tool and what label the tool result produces.

@strahl.tool(
    requires=Label(source={"user"}, visibility={"user"}),
    produces=Label(source={"crm"}, visibility=lambda customer_id: {f"customer:{customer_id}"}),
)
def lookup_customer(customer_id: str) -> str:
    ...

requires — the label that must be satisfied for information to select the tool. If params is omitted, every argument inherits this label. If the influencing information does not carry the required tags, Strahl denies the call.

params — optional per-parameter requirements. If provided, every declared top-level parameter must be listed, including optional parameters. Omitted optional arguments do not create argument sinks for that call.

produces — the label assigned to the tool result. This label flows forward into subsequent analysis, so the result of one tool can influence later tool checks.

Use params when the arguments do not all share the same boundary:

@strahl.tool(
    requires=Label(source={"assistant"}, visibility={"user"}),
    params=dict(
        customer_id=Label(source={"support-agent"}, visibility={"support-agent"}),
        message=Label(source={"support-agent"}, visibility=lambda customer_id: {f"customer:{customer_id}"}),
    ),
    produces=Label(source={"support-agent"}, visibility=lambda customer_id: {f"customer:{customer_id}"}),
)
def reply_to_customer(customer_id: str, message: str) -> None:
    ...

Role labels

Every non-tool-response message role must have a label. Role labels describe the trust level of the message author.

strahl.set_role_labels({
    "user":      Label(source={"user"},      visibility={"user"}),
    "assistant": Label(source={"assistant"}, visibility={"user"}),
    "system":    Label(source={"system"},    visibility={"internal"}),
})

Role labels must be static — they cannot use callable tag set expressions, because they apply to message roles, not individual tool arguments.

Documents

Documents are named pieces of context added to the conversation separately from the message transcript — retrieved content, policies, runbooks, scraped pages.

strahl.add_document(
    "ticket-123",
    ticket_body,
    label=Label(source={"customer:A"}, visibility={"customer:A", "support"}),
)

Like role labels, document labels must be static. A document exists before any tool call, so its label cannot depend on future arguments.

How analysis works

When you call strahl.analyze(messages), the SDK:

  1. Parses the transcript into typed messages with resolved labels on each tool call and tool result.
  2. Posts the labeled transcript, documents, and role labels to the Strahl API.
  3. Returns an AnalyzeResponse with a per-tool-call decision, a per-argument breakdown, and details about which earlier message or tool result influenced a blocked call.

If the final assistant message has no tool calls, the SDK short-circuits and returns a local permitted result without calling the API.

Designing your label space

The main design task when adopting Strahl is deciding which tags must be present for each sensitive action. A few principles:

Start with the action, not the taxonomy. For each sensitive tool, ask: what must be true of the information driving this call? Name a tag for that property, and put it in requires.source.

Use the same string wherever two things should match. If a user message and a tool flow policy both use "user", they match. If one uses "user" and the other uses "end-user", they do not.

Don't infer from semantics. Strahl does not know that "admin" outranks "user", or that "internal" implies "employee". Design your tags so the relationship you want is expressed by set membership, not word meaning.

Compose with normal Python. Tag sets are sets. You can define constants and combine them with |, &, and -:

USER = {"user"}
SUPPORT = {"support-agent"}
INTERNAL = {"internal"}

def CUSTOMER(customer_id: str) -> set[str]:
    return {f"customer:{customer_id}"}

# A tool visible to both the user and support agents
visibility = USER | SUPPORT