Governing an Agent from Any Platform
An agent does not have to be built in Beak to be governed by Beak. An agent written on LangGraph, CrewAI, LangChain.js, Mastra, or nothing at all can put itself in your governed inventory with one call at startup, and from then on it appears in Agent Governance beside the agents you built here.
What Beak can actually do for that agent, though, depends on something a registration cannot change: which of Beak's boundaries the agent's traffic crosses. An agent that only tells Beak it exists can be inventoried and identified. An agent whose model calls route through the Beak AI Gateway can additionally have its prompts inspected, its answers scanned, and its spend attributed. This page is about that distinction, because it is the difference between an agent that is governed and an agent that is merely listed.
What a boundary is, and why it decides everything
A boundary is a point where the agent's traffic passes through Beak. There are five, and they are the only five:
gateway.llm— Model calls route through Beak AI Gateway on a Beak virtual key.hub.tools— Tool and MCP calls go through Beak AI Hub and authorize against the agent's own scopes. Beak proxies the call and sees its result.tool.hook— The agent's platform asks Beak before it runs a tool, and runs it or not on Beak's answer. Microsoft Copilot Studio's external threat detection is this boundary today. Beak sees the tool definition, its inputs, the planner's reasoning and the user message that triggered the turn; it never sees the tool's result, and it sees nothing on a turn that calls no tool.inference.hook— The model vendor calls Beak's inference hook before completing a request.registry.only— Beak holds a record and nothing more.
hub.tools and tool.hook are kept apart on purpose. Both authorize tool calls against the agent's
scopes, but only the proxy can act on what came back, so reporting them as one boundary would claim
result-side inspection for an agent whose platform never shows Beak a result.
Governance follows from those crossings and from nothing else. Two planes come from registering at all, because Beak can always say an agent exists and who it is:
| Plane | What it means | Granted by |
|---|---|---|
inventory | The agent is in the registry and counted in coverage figures | registration itself |
identity | The agent has a Beak identity and can be given an autonomy grant | registration itself |
tool_authz | Tool and MCP calls authorize against the agent's scopes | hub.tools, tool.hook |
content_runtime | Rule-engine policy applies to the agent's traffic | gateway.llm, hub.tools, tool.hook, inference.hook |
content_prompt | Prompts are inspected before the model sees them | gateway.llm, inference.hook, tool.hook (on tool-calling turns only) |
content_completion | Answers are scanned after the model returns them | gateway.llm |
cost | Spend, budget, and rate limits are attributed to this agent | gateway.llm |
Read the table in the direction that matters: content_completion and cost are only ever
available through gateway.llm. No amount of registering, adopting, or configuring produces them,
because there is no other point at which Beak sees the answer or the bill. If you need either one for
an agent, that is a change to how the agent is deployed, not a setting on this page.
Note:
registry.onlygrants nothing beyondinventoryandidentity. It exists as a value you can state explicitly, so that "Beak holds a record and nothing more" reads as a decision somebody made rather than a field somebody forgot.
Declaring is not granting
When an agent registers, it may declare the boundaries it intends to cross. A declaration is a request, not a fact:
- Declared boundaries appear as
pending. Nothing about the agent's governance has changed. - Granted boundaries are ones Beak has issued the credential for — a Gateway virtual key, a Hub connection. Only these raise what is governed.
This split is deliberate and it is the security property of the whole model. If declaring were
enough, an agent could assert its own governance: it would name gateway.llm, Beak would report its
prompts as inspected, and nothing would be inspecting them. The dashboard would read as covered while
the estate was not. So the agent states intent, and Beak — never the agent — decides when that intent
became real.
Warning: A declaration is visible in the console as a pending request, and an agent with every boundary declared and none granted is governed exactly as much as an agent that declared nothing. Read the granted column, not the declared one.
What the tier name means, and when to ignore it
Beak reports a governance tier as a shorthand for a whole capability set:
| Tier | Planes | Typically |
|---|---|---|
managed | all seven | An agent whose model calls and tool calls both route through Beak |
brokered | inventory, identity, tool_authz, content_runtime, content_prompt | A Copilot Studio agent reached through Beak's external-security frame |
inspected | inventory, identity, content_runtime, content_prompt | An estate covered by Anthropic inference hooks |
registered | inventory, identity | Agent 365, Power Platform, and other third-party listings |
custom | whatever the boundaries grant | A combination matching none of the four above |
custom is not a lesser tier. It is what Beak reports when the honest answer is a set the four
preset names cannot express — an agent holding six of the seven planes, for instance. Beak reports it
rather than rounding to the nearest preset, because rounding down understates coverage you have and
rounding up claims coverage you do not.
This is why the console shows a capability matrix rather than only a tier badge, and why any integration reading this over the API should branch on the individual planes:
# Right: ask what is governed.
if profile.governs("content_prompt"):
...
# Wrong: a `custom` agent is in no per-tier lookup table, so this under-reports
# an agent holding six planes out of seven.
if profile.governance_tier == "managed":
...
registered is also the default for an agent that declares no source at all. A source Beak cannot
identify must under-claim rather than over-claim, so the floor — not the ceiling — is what an
unrecognised agent gets.
Registering an agent
Two SDKs cover the frameworks agents are actually written on. Both are idempotent on
(tenant, source, external_id), so calling on every boot is the intended use — it is how a
redeployed agent re-asserts itself with no operator step.
Python
from beakagentidlib import Declaration, configure, register_agent
configure(
security_url="http://beak-aisecurity:5000",
service_code="orders",
environment="prod",
)
profile = await register_agent(
tenant_id=42,
agent_id="orders-triage",
name="Orders Triage",
source="langgraph",
external_id="svc-orders/triage@v3",
declares=Declaration(boundaries=("gateway.llm",), runtime="customer_hosted"),
)
profile.pending # ("gateway.llm",) — asked for, not yet issued
profile.governs("content_prompt") # False, until Beak issues the key
TypeScript
import { configure, governs, registerAgent } from '@beak/agent-identity';
configure({
securityUrl: 'http://beak-aisecurity:5000',
serviceCode: 'orders',
environment: 'prod',
});
const profile = await registerAgent({
tenantId: 42,
agentId: 'orders-triage',
name: 'Orders Triage',
source: 'langgraph',
externalId: 'svc-orders/triage@v3',
declares: { boundaries: ['gateway.llm'], runtime: 'customer_hosted' },
});
governs(profile, 'cost'); // false
tenant_id, source, and external_id are required with no defaults. The governance record is
keyed on all three, and a vendor's own id is unique only inside one connection — without the tenant,
two customers whose connectors discover the same agent id would share one record.
Adopting the SDK changes nothing until you say so
Both SDKs default to mode: "log". In that mode a governance outage writes a loud warning and
returns a degraded profile; it does not stop your agent from starting. Switch to enforce when you
want a failed registration to be fatal.
Two behaviours are worth knowing before you wire this in:
- A degraded profile is not an answer about your estate. Every plane reads
falseon it, because Beak could not be reached — not because nothing is governed. The warning says so explicitly. - A misconfiguration is fatal in both modes. A missing
security_urlcan never succeed on retry, so it raises rather than degrading. Degrading it would leave the agent permanently ungoverned behind a log line reporting that everything was fine.
Agents synced through a connector
An agent brought in by a Runtime connector — Amazon Bedrock, Salesforce Agentforce, and the others — is registered for governance automatically as part of each sync, with the provider type as its source. Nothing is required of the agent's author.
This is off until an operator sets AGENT_GOVERNANCE_SECURITY_URL on Beak AI Agent Runtime. Until
then connector syncs behave exactly as they did before: the agent is invocable and has no governance
record.
How a boundary becomes granted
Only Beak records a grant, and it does so when it has issued or verified the credential that puts the agent's traffic on the boundary. Nothing an agent or an operator types raises a tier.
| Boundary | Recorded when |
|---|---|
gateway.llm | Beak AI Gateway binds a virtual key to the agent's identity |
hub.tools | Beak AI Hub creates an MCP connection owned by the agent |
tool.hook | The agent's platform makes its first verified, in-scope call to Beak's tool hook — for Copilot Studio, the first analyze-tool-execution call from the tenant's registered app that Beak actually inspected |
inference.hook | Beak verifies the vendor's first signed hook call for the agent |
registry.only | Never granted; it is the statement that nothing else will be |
Each recording service proves it is a Beak service with a signed workload assertion, so "Beak issued this credential" can be asserted by Beak and by nobody else. A call the platform made but Beak did not inspect — an environment outside the tenant's configured scope, a frame Beak could not parse — records nothing, because granting a boundary on it would claim governance that did not happen.
The three integration levels
The boundaries above compose into three levels, which is how the onboarding wizard and the API describe how deeply Beak governs an agent:
| Level | Boundaries | Tier it derives to | What Beak sees | What Beak cannot see |
|---|---|---|---|---|
| Full Gateway | gateway.llm + hub.tools | managed | Prompts, completions, tokens and cost, every tool call and its result | Nothing on the model or tool path |
| Inference hook | inference.hook | inspected | The prompt side of each model call | Completions and cost — the vendor meters the call |
| Tool hook | tool.hook | brokered | Tool inputs, planner rationale, the outputs of earlier tool calls the platform carries in the request, the user message on tool-calling turns | Turns with no tool call, completions, cost, the result of the call being authorized |
Not every framework or platform can reach every level, and the reason differs in kind. GET /aisecurity/agents/onboarding-catalog states, per source, which levels are reachable and by what
mechanism, with two separate flags:
reachable: false— The source's own protocol has no hook of that shape. The OpenAI Agents SDK, the Claude Agent SDK, LlamaIndex, AutoGen and the low-code builders expose no blocking per-model-call hook, so an inference hook cannot be wired in-process for them; the Full Gateway or a tool hook is the honest choice.available: false— The source could reach it, but Beak has not shipped the receiving half yet. The note names the gap, for example the Anthropic Messages format that Claude Code needs from the Gateway.
Note: A level is a set of boundaries, nothing more. Choosing one declares intent; the capabilities appear only when the boundary is granted as described above.
Broker tokens for external tool dispatchers
POST /aisecurity/tools/authorize answers "may this agent call this tool" and is meant to be
called by runtimes Beak does not run. A caller outside the Beak cluster must present a broker
token — Authorization: Bearer bkr_... — minted by an operator with the agents:keys permission:
POST /aisecurity/brokers/tokens
{ "name": "orders-runtime" }
The token is shown once in the response and stored hashed; list tokens with GET /aisecurity/brokers/tokens and revoke one with DELETE /aisecurity/brokers/tokens/{id}.
A broker token authenticates the caller and fixes the tenant: the decision is still made from
the agent's own profile, and the tenantid header is ignored when a token is presented, so a token
for one tenant cannot be pointed at another. Nothing is inferred from the network: a caller that
presents no proof is refused wherever the request came from. Beak's own services — the Agent
Runtime and the Hub — prove themselves instead with an Ed25519 workload assertion (the same
credential they use toward BEAKIAM) and name the tenant in the header; Security holds their public
keys as trust anchors per environment.
Note:
TOOL_AUTHZ_ALLOW_UNAUTHENTICATED_IN_CLUSTERlets a cluster whose Runtime and Hub do not sign yet keep working during rollout. It is off by default, and every answer given under it is logged as unauthenticated. Roll out in this order: deploy Security with the switch on, register the Runtime's and the Hub's public keys as trust anchors for the platform tenant, deploy the Runtime and the Hub with their signing keys, then turn the switch off. Turning it off before both services sign refuses every tool call made through Runtime-managed agents and Hub connections.
The tool-hook receiver
Any platform that can call a URL before it runs a tool can put Beak on the tool.hook
boundary. The receiver is one endpoint that understands four payload shapes and answers in
the caller's own shape:
POST /aisecurity/hooks/tool?format=<beak|claude_code|mcp|agent_hooks>
Authorization: Bearer bkr_...
X-Beak-Agent-Identity: <BEAKIAM identity id> (or X-Beak-Agent-Id: <registry id>)
X-Beak-Mcp-Server: <server slug> (format=mcp only)
format | What the platform sends | What Beak answers |
|---|---|---|
beak | Beak's frame: agent_id or agent_identity_id, tool {name, server, required_scope}, inputs, context {user_message, planner_thought, prior_tool_outputs} | {allowed, decision, reason, code, resume_token?} |
claude_code | Claude Code's PreToolUse hook input (tool_name, tool_input, session_id); mcp__server__tool names are split into server and tool | hookSpecificOutput.permissionDecision of allow, deny or ask, with the reason |
mcp | A JSON-RPC tools/call request — what an MCP proxy or a Bedrock AgentCore Gateway interceptor forwards | A JSON-RPC result, or error -32001 carrying the reason code (and a resume token on escalation) |
agent_hooks | An AGENT-HOOKS-0.1 pre_tool_call interception (agent.id, tool.name, tool.arguments); best-effort against the 0.1 contract | decision of ALLOW, DENY or ESCALATE with the reason |
The beak frame also takes the result of a tool that already ran: direction: "output" with
result. Authorization no longer applies — the call happened — so Beak scans the result and answers
allow, redact with redacted_text, or block, meaning do not hand this result to the model. It
is the one place indirect prompt injection or a leaked secret in a tool result can still be stopped
before the next turn. Every tool-hook call, allowed or not, now leaves a row on the same ledger the
prompt scan writes, with the direction and the tool in its metadata.
Every format runs the same decision, in this order:
- Tool authorization — the agent's own scopes, allow and deny lists and governance
state, exactly as
/aisecurity/tools/authorizedecides. An unknown or ungoverned agent is refused. This check fails closed. - Content — the frame's text (user message, planner rationale, prior tool outputs, the
tool's inputs) through the moderation pipeline against the tenant's tool policies. A
blockrefuses; aredactalso refuses, because a hook cannot rewrite what the platform is about to run. If Beak's own pipeline fails, the call is allowed and the verdict saysscan_failed_open: true— content inspection fails open with an alarm, like every other content boundary. - Escalation — when the agent's profile lists the tool under
permissions.tools.require_approval, the answer isescalatewith a resume token. The caller pollsGET /aisecurity/hooks/tool/approvals/{token}; an operator sees the request under pending approvals and decides it withagents:authorize. Approvals expire after 30 minutes and an expired token reads asexpired, never as approved. If the escalation cannot be recorded, the call is refused.
Every verified call records the agent as seen at the tool boundary and grants
tool.hook — the crossing is the evidence — and every refusal leaves an audit row.
Note: The receiver always answers
200. Each of these protocols treats a non-2xx as "the hook failed" and then applies its own default, which is usually to run the tool. A refusal Beak chooses always beats a status code Beak emits.
Note: Microsoft Copilot Studio keeps its dedicated endpoint (
/aisecurity/agentsecurity/{slug}/analyze-tool-execution): its 1-second budget and Entra app authentication are protocol-specific. It grants the sametool.hookboundary.
The prompt hook
The inference-hook level needs someone to ask Beak about a prompt before the model sees it. Inside the cluster that is the Gateway. Outside it — middleware running in your own LangChain, ADK or CrewAI process — the same scan is reachable behind a broker token:
POST /aisecurity/hooks/prompt
Authorization: Bearer bkr_...
{
"messages": [{"role": "user", "content": "..."}],
"agent_id": "acme:orders-triage", // or "agent_identity_id": "<BEAKIAM identity id>"
"agent_metadata": {"source": "langgraph", "model": "gpt-4o"}
}
The tenant comes from the token — a tenantid header is ignored — and the request then runs the
inline scan unchanged: the same pipeline, the same rule engine, the same ledger row and the same verdict
(action of allow, warn, log, redact or block, with risk_score and scan_scope) that the
Gateway's own traffic gets. Without a token nothing is scanned and the call is refused.
Every verified call records the agent named in the body as seen at the inference boundary and grants
inference.hook. The grant is the consequence of the call, not a prerequisite for it.
The same route takes the completion too: send direction: "output" with the model's response.
By itself that is a detection pass — the answer may already be on its way — so a policy that fired is
recorded as would_block and the response says enforced: false. A caller that holds the completion
until Beak answers says so with caller_applies_verdict: true; then block stays block, enforced
is true, and the audit row records an enforcement, because one happened. The Gateway never sets the
flag, so its completion scans are unchanged. Hook-sourced rows keep the whole scanned span, not the
Gateway's excerpt: for a platform calling in, the row is the only record of what was said.
Note: Tokens and cost stay with the vendor that meters the call; that is what separates the inference-hook level from the Full Gateway.
Framework hooks: beakagenthooks
For agents built on LangChain, Google ADK or CrewAI, the beakagenthooks library wires both hooks in
one line per framework. It is published to the Beak package feed; install the extra for your framework:
pip install "beakagenthooks[langchain]" # or [adk] or [crewai]
Configure it once per process, then attach the framework's adapter:
import beakagenthooks as hooks
hooks.configure(
security_url="https://<tenant>.beakops.com/api",
broker_token=os.environ["BEAK_BROKER_TOKEN"],
agent_id="acme:orders-triage", # or agent_identity_id=<BEAKIAM identity id>
source="langgraph",
mode="log", # start here; switch to "enforce" when the log is clean
)
# LangChain 1.x — middleware on the agent
from beakagenthooks.langchain import BeakGovernanceMiddleware
agent = create_agent(model, tools, middleware=[BeakGovernanceMiddleware()])
# Google ADK — a plugin on the runner, so it covers every agent in the app
from beakagenthooks.adk import BeakPlugin
runner = Runner(agent=root_agent, app_name="orders", plugins=[BeakPlugin()], ...)
# CrewAI — process-wide execution hooks
from beakagenthooks.crewai import install
install()
| Before | The library calls | On refusal in enforce | If Beak cannot be reached |
|---|---|---|---|
| Each model call | POST /aisecurity/hooks/prompt | The model is not called; LangChain raises PromptBlocked, ADK returns a refusal response, CrewAI aborts the step | The call proceeds and an error log names the uninspected prompt |
| Each tool call | POST /aisecurity/hooks/tool?format=beak | The tool does not run; LangChain hands the model a tool message with the reason, ADK returns an error result, CrewAI aborts the step | The tool is refused — authorization fails closed |
In mode="log" nothing is ever refused: each would-be refusal is logged as a warning so you can read
what enforcing would have changed before it changes anything. escalate (a tool the agent's profile
reserves for a human) counts as a refusal in these synchronous hooks; the verdict carries the resume
token for a runtime that can poll the approval.
Note: Adding the library grants nothing. The first prompt it sends earns
inference.hook, the first tool call it sends earnstool.hook, and the agent's tier follows — which is what the wizard's verify step waits for.
Finding the agents nobody onboarded
Two doors bring agents into the registry before anyone fills in the wizard. Both produce
candidates — inventory and identity, coverage observed, nothing enforced — because
knowing an agent exists is not the same as standing on its call path.
Platform connectors. Eight discovery integrations enumerate the agents a platform already knows about: Power Platform (Copilot Studio), Amazon Bedrock, Amazon Bedrock AgentCore, Azure AI Foundry Agent Service, Google Vertex AI Agent Engine, iGentic, Salesforce Agentforce and A2A agent cards. Every platform with a saved credential is also re-enumerated on the background discovery tick, so an agent first seen in traffic picks up its display name without anyone clicking Discover. Each one's detail page states the least privilege it needs, read from the connector itself. See Integrations.
Governing agents discovered from iGentic
Discovery lists iGentic's orchestrators and agents as candidates. From there the operator chooses one of two modes per agent, and the deployment holds one integration key for both:
- Gateway mode (Full Gateway). iGentic's Azure OpenAI client points at the Gateway's Azure-shaped route with the integration key as its API key and the agent's GUID in a header; Beak resolves the per-agent key it holds for that agent. Prompts, completions, tokens and cost are the agent's.
- Hooks mode (Level 2 and Level 3 together, chosen as Both hooks). iGentic calls the prompt hook before the model and again with the completion, and the tool-hook receiver before each function runs and again with its result. Completion and result verdicts are enforcements, and every call is on the ledger.
- Passive telemetry works in either mode. Point iGentic's OpenTelemetry exporter at
POST /aisecurity/hooks/otlp/v1/tracesand every run populates the inventory atobserved.
The contract, the settings, and the operator sequence are on iGentic. Until an agent is adopted and wired, it is inventory and identity: assign it an owner, and the posture panel counts it as discovered, not onboarded after 14 days so it does not quietly read as governed.
The passive OpenTelemetry receiver. Most agent frameworks and observability SDKs — LangSmith,
Pydantic AI, the OpenAI Agents SDK, Google ADK, Datadog — already export gen_ai.* spans. Point an
exporter at Beak and every agent that runs shows up, with the tools it called and the models it
used, at the cost of one environment variable:
OTEL_EXPORTER_OTLP_PROTOCOL=http/json
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://<tenant>.beakops.com/api/aisecurity/hooks/otlp/v1/traces
OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer bkr_..."
The receiver reads OTLP/HTTP JSON only; a protobuf exporter is refused with a 415 that names
the setting above, rather than acknowledged unread. The tenant comes from the broker token. Spans
whose gen_ai.operation.name is an agent or model operation (invoke_agent, execute_tool,
chat, …) name the agent from gen_ai.agent.id or gen_ai.agent.name, falling back to the
resource's service.name; the provider is mapped to a catalog source where that is unambiguous and
kept verbatim otherwise, so the inventory says what the exporter said. A classic Bedrock agent seen
through telemetry lands under the same bedrock source the Bedrock connector uses, so it stays one
agent in the registry. Batches larger than the receiver's limit are refused with a 413; the limit
defaults to 4 MiB and is set on Beak AI Security with OTLP_RECEIVER_MAX_BODY_BYTES.
Note: A span proves an agent exists and what it did. It cannot stop anything, so an agent seen only through telemetry is recorded at coverage
observedand granted no boundary — it must never read as protection. Onboard it through the wizard to put Beak on its path.
The onboarding wizard
Agent Registry → Onboard agent walks the same contract in seven steps. Everything the wizard says is read from the service; it predicts no capability.
- Where does the agent run? — Pick a platform, a framework, or Custom framework from the catalog. Each tile says how many of the three levels are available today. Declare it by hand keeps the raw boundary picker for a source the catalog does not list.
- How deep should Beak govern it? — Three level cards. Each lists the planes the level would govern, once credentialled, with the absent ones stated in words. A card the source's own protocol cannot reach is disabled and says so ("Not possible for OpenAI Agents SDK — no blocking per-model-call hook"); a card Beak has not shipped the receiving half for is disabled and names the gap ("Not yet available in Beak — …"). The two are different advice, and the card says which.
- Register — Agent id, name, and the owner (required — the accountable person every finding about this agent names). The level is the declaration, so the boundaries are shown, not re-ticked.
- Result and credential — What the service actually returned: tier, granted and pending boundaries, the capability list. Then the credential the level needs, shown once: an enrolment token for Full Gateway on an agent Beak does not host (the identity comes from enrolling; the Gateway key is bound to it afterwards under Keys), a broker token for a middleware or webhook level, or a pointer to the Integrations page for a platform hook.
- Integrate — The verified snippet for the chosen framework and level, with placeholders for your Beak host and the credential, plus the registration call. A level with no verified snippet yet shows the catalog's note rather than invented code.
- Verify — Beak polls for the first governed event on this agent every five seconds while the step is open and shows it when it arrives. Until then the agent is pending integration, not protected. The coverage ladder underneath dates each rung the agent has reached and labels each one it has not.
- Mode — The agent starts in log mode. The step says where enforcement is switched for each level and how that level fails, because "protected" is easy to read as fail-closed.
An Integrations page for a platform Beak knows links straight into step 2 with that platform chosen (Onboard an agent from this platform), so an operator who has just connected a platform is not asked to find it again.
Agents a connector or a hook already found take the same path. The first screen offers Already discovered — adopt it, listing every candidate a platform connector enumerated or a hook saw before anyone onboarded it, with the source it came from. Choosing Onboard on a candidate does not adopt it on the spot: it continues into step 2 for that candidate's source, then step 3 with the id and name filled in from the discovered record and the owner still required, and on through result, integrate, verify and mode. A Beak AI Runtime candidate can be issued its Gateway credential at step 3 by choosing the Gateway team whose budget it counts against. Adoption records the owner and the declared level in the same transaction as the governance change, so an agent that arrived through a connector is described exactly like one registered by hand — and the posture panel's declared a level but never crossed it finding applies to it in the same way. Exclude still removes a candidate from the list without onboarding it.
Note: The wizard cannot make a level real. Choosing one declares intent and issues the credential; the level's capabilities appear only when traffic arrives on that credential and Beak records the grant, as described above.
Posture: what is not protected yet, and the two clocks
Every earlier section describes a way to put Beak on an agent's path. This one is about the
gap between that and what a console reads as protected. Agent Governance shows an
Onboarding posture panel, served by GET /aisecurity/agents/onboarding-posture, computed
from the tenant's own evidence on every load — never stored, never estimated.
Findings are conditions that look governed and are not, each with the count behind it and what to change:
| Finding | Severity | What it means |
|---|---|---|
| Copilot Studio in log mode | High | Every tool call is evaluated and every one is allowed. The integration reads as connected while nothing is prevented. |
| Copilot Studio enforcing, allow on timeout | Medium | Enforcement is on, but a request that breaches Copilot's one-second budget lets the tool run. A cold worker and a warm one give opposite outcomes on the same content. |
| Decisions proceeded uninspected | High or Medium | Prompt decisions in the last 24 hours that were allowed because the policy store could not be reached. On the wire they are indistinguishable from a clean pass. |
| Foundry agents on the Gateway with no inline policy | High | Microsoft's content filters do not apply to bring-your-own-model traffic, and no API-guardrail or hybrid policy is active, so those prompts are inspected by nobody. |
| Refused at the Gateway, not adopted | Medium | In the last 24 hours an integration key named an agent that has no adopted key behind it, and the Gateway refused the call. The agent is live and trying to call an LLM while ungoverned. Named when a connector already holds its profile, otherwise shown by its platform id. Onboard it; the per-agent key is minted at adoption. |
| Declared a level, never crossed it | Medium | Registered more than a day ago with an integration level declared and no boundary granted. The integrate step was never completed; the level is intent, not coverage. |
| Discovered, not onboarded | Low | Found by a connector or telemetry and still at coverage observed after fourteen days. |
The two clocks measure the promise the onboarding experience makes, per tenant:
- Minutes to inventory — from saving a discovery integration to its first candidate appearing in the registry. One figure per connected platform, and the median across them.
- Minutes to first protected event — from an agent's registration to the first governed decision recorded against it, as a median over the last thirty days of onboardings, with the number of agents still waiting for their first event.
Both clocks answer null until there is something to measure, and show how many samples the
median stands on. A clock with no samples is not a fast clock.
Note: Fail-open is the market default at the tool boundary — Copilot Studio's one-second budget, Model Armor on unavailability — and Beak's own prompt scan fails open with an alarm by design (a policy-store outage must not stop every agent from thinking). The posture panel is where that design choice becomes a line item rather than a footnote: every level card states how it fails, and this panel says how often it did.
Best Practices
- Register on every boot, not once at provisioning. It is idempotent by design, and a redeployment that skips it leaves a stale record nobody notices.
- Declare only what the agent really crosses. A declaration is a request Beak will show as pending; declaring everything produces a queue of requests that were never true and hides the one that is.
- Read the capability matrix, not the tier badge. The badge is a summary, and for a
customagent it is a summary no lookup table can expand. - Treat
registry.onlyas a real answer. For an agent you want inventoried and nothing more, stating it is better than leaving the field empty and letting a reader wonder. - Give the agent its own
external_id. It is half the idempotency key; without it, two agents from the same source can collapse into one record.