Skip to content

Python: [Feature]: Let a tool declare standing guidance the middleware appends to its results #8757

Description

Description

LabelTrackingFunctionMiddleware hides an untrusted result behind a VariableReferenceContent, which is the right behaviour and the reason the feature is worth having. Hiding is silent, though: a hidden failed compile and a hidden clean compile are the same [var_…] to the model. A tool that knows what its hidden output means has no supported way to say so.

This is a gap 1.19 opened deliberately, and the deliberate part is why it needs a published answer rather than a workaround. Until 1.19 a tool could return one item labelled trusted beside its untrusted output, and _extract_content_label took that item's integrity outright. 1.19 made a per-item label restrict-only — combine_labels(fallback_label, embedded_label) — unless the item carries the framework's own private authoritative marker. That change closes a real hole: a tool could otherwise promote content it produced at call time, so bytes from a web page could be marked trusted and land in the conversation with authority. It also removes the only channel a tool had for a sentence about its own hidden output.

Standing guidance cannot carry what the restriction was defending against, and the difference is structural rather than a matter of degree:

  • The text is fixed before the call exists. It comes from the tool definition, so it cannot vary with arguments, with what the body read, or with anything a sandbox or a remote service produced. An attacker who controls every byte the tool touches at runtime controls none of it.
  • The framework can be the producer. If the middleware appends the sentences out of additional_properties, the trusted item never passes through the tool body at all — so this needs no authority grant to a third party and no marker. It is framework-owned content by the same standard quarantined_llm's own stamp meets.
  • It is bounded and auditable. A host can read the declared text off every tool it attaches and see, before running anything, the complete set of text that will ever be trusted.

Two mechanisms already in the framework make this a small addition rather than a new idea. SECURITY_TOOL_INSTRUCTIONS is standing text the framework injects to explain what a variable reference is and what to do about it, so the premise — hidden content needs trusted standing text beside it — is already accepted; what is missing is that the block is global, and the useful sentence is per-tool. And apply_mcp_security_labels(..., trust_server_ifc=True) already makes a result label authoritative when local configuration asks for it, under the rule stated beside it: “Local configuration controls result-label authority.” This request needs less than that grants, because the content is fixed at declaration rather than arriving over a wire.

What is being asked for is one entry a tool may declare:

@tool(additional_properties={
    "source_integrity": "untrusted",
    "standing_guidance": ["A result you cannot read is not a clean validation."],
})
async def validate(files: list[str]) -> str: ...

LabelTrackingFunctionMiddleware appends each sentence to that tool's result as its own Content, labelled trusted at the tool's own confidentiality, after processing the items the body returned. The body never returns them and cannot alter them. A host opt-in alongside it would be reasonable and is not required by the argument above.

Scope. This affects any host that installs LabelTrackingFunctionMiddleware and any tool declaring source_integrity="untrusted" — which is the conservative declaration such a tool should be making. The labels ride in additional_properties and nothing else reads them, so the loss lands exactly on the deployment that turned the security feature on.

What integrators do instead today, none of it good: stamp the private _INTERNAL_RESULT_MARKER, which is a third party impersonating a framework-owned producer, widens the item's reach through allow_principals=True on the same parse, and rests on a name that changed between 1.18 and 1.19; declare the whole tool trusted and label every derived item untrusted, which works on both cores and inverts the direction of failure, since a label the framework cannot parse then falls through to a trusted declaration; or move the sentence into the tool description, which is safe and gives up adjacency to the result it is about.

Code Sample

import asyncio

from agent_framework import Content, FunctionInvocationContext, FunctionTool
from agent_framework.security import LabelTrackingFunctionMiddleware

GUIDANCE = "A result you cannot read is not a clean validation."


async def validate(files: list[str]) -> list[Content]:
    return [
        Content.from_text("compiler output the model must not act on"),
        Content.from_text(
            GUIDANCE,
            additional_properties={
                "security_label": {"integrity": "trusted", "confidentiality": "public"}
            },
        ),
    ]


tool = FunctionTool(
    name="validate", func=validate, additional_properties={"source_integrity": "untrusted"}
)
middleware = LabelTrackingFunctionMiddleware()
context = FunctionInvocationContext(function=tool, arguments={"files": ["main.bicep"]})


async def call_next() -> None:
    context.result = await tool.invoke(arguments=context.arguments)


asyncio.run(middleware.process(context, call_next))
for item in context.result:
    hidden = (item.additional_properties or {}).get("_variable_reference")
    print("hidden" if hidden else repr(item.text))



# agent-framework-core 1.18.0
hidden
'A result you cannot read is not a clean validation.'

# agent-framework-core 1.19.0
hidden
hidden

Language/SDK

Both

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agentmiddlewareUsage: [Issues, PRs], Target: middleware related featurespythonUsage: [Issues, PRs], Target: Python

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions