Lab 5 · Domain 3 · 19%

Integration Protocols, Auth & Observability

Domain 3 is the heaviest domain on the exam: twelve of the sixty-three items. It is where an architecture stops being a diagram and starts touching other people's systems, other people's data, and other people's blast radius. The questions here are not "can you wire up MCP." They are "given four ways to reduce this risk, which one is actually the control, and which three only watch it happen." This is the second of Domain 3's two labs.

Answer key Lab5_Complete.ipynb
Exam objectives covered
  • Evaluate tool/agent configuration for capability bloat
  • Analyze authentication and authorization requirements to identify security gaps
  • Evaluate accuracy-latency trade-offs and justify configuration decisions
  • Analyze observability challenges and select monitoring strategies at scale
  • Evaluate connection protocols and select the appropriate integration mechanism (MCP, API/CLI, agent-to-agent)
Least privilege removes the capability; it does not guard it. This is the single most reliable pattern in Domain 3. When one option deletes a tool and the others log it, prompt before it, or upgrade the model behind it, the deleting option wins. Logging is detective, a confirmation prompt is compensating, a bigger model is unrelated to scope. Only removal changes what the agent can do.

1. Capability Bloat and Least Privilege

The exam guide's own Sample 1 is worth teaching verbatim, because it encodes the entire mental model for this domain in a single item:

The guide's Sample 1
A customer-support agent can read tickets, draft replies, issue refunds, and delete user accounts. Support staff only ever need to read tickets and draft replies. Applying least privilege, which change best reduces risk?

The correct answer is to remove the refund and delete tools from the agent's configuration entirely. Not to log their use. Not to add a confirmation prompt. Not to move to a more capable model that "makes fewer mistakes." Sit with why each of those is wrong, because the distractors are the lesson:

OptionControl typeWhy it loses
Remove the two toolsPreventive — least privilegeThe capability no longer exists. Nothing to exploit, misfire, or be socially-engineered into.
Log every refund and deletionDetectiveYou find out after the account is gone. Logging is how you learn you had the wrong architecture.
Add a human confirmation promptCompensatingBetter than nothing, but the capability is still wired in, one prompt-injection or fatigued click away from firing.
Use a more capable modelUnrelatedModel quality has nothing to do with authorization scope. A smarter model with the delete tool can still delete.

Generalize the pattern: when one option removes a capability and the others monitor it, the removing option wins. The staff never needed refund or delete, so neither does the agent acting on their behalf. Grant is the exception that must be justified; the default is deny.

Bloat is a correctness problem, not only a security one

There is a second reason the exam keeps deleting tools, and candidates who think of this purely as a security concern miss it: more tools degrade tool-selection accuracy. Every tool you expose is another option the model weighs on every turn, another schema competing for attention, another chance to pick the plausible-but-wrong action. A twenty-tool agent is not just twenty tools' worth of attack surface; it is measurably worse at choosing among them than a five-tool agent. Pruning the surface makes the agent both safer and more correct. When an item asks how to improve a flailing agent's reliability, "give it fewer, sharper tools" is frequently the answer, not "give it more."

The tool surface is a prompt. Tool definitions render before the system prompt and consume the same attention budget. A bloated tool list is a bloated prompt with a security liability attached. If you cannot name the user task each tool serves, it is not a capability, it is a distractor you shipped to production.

2. Authentication vs. Authorization: The Gap Analysis

The objective is phrased as "analyze authentication and authorization requirements to identify security gaps." The exam is testing whether you can tell the two apart and spot where a design conflates them. Authentication answers who is calling. Authorization answers what this caller is allowed to do. An agent that authenticates flawlessly and authorizes nothing is the most common failure in the domain.

The governing rule, and the sentence to carry into the exam:

The agent must act with the permissions of the requesting user, not of the application. If the agent authenticates as one omnipotent service account and serves every user through it, you have built a confused deputy: any user can reach any data the service account can reach, because the model, not the identity layer, is the only thing deciding who sees what. That is not an authorization system. That is a suggestion.
Common gapWhat it looks likeThe fix
Confused deputyAgent runs as a service account holding the union of all users' permissions; per-user scoping lives only in the promptPropagate the end user's identity; authorize each tool call against that user's grants
No per-call authorizationAccess is checked at login, never again; the agent then calls any tool with any argumentsCheck authorization inside each tool, on the actual arguments, every call
Unbounded tokensTokens with no audience and no expiry: a stolen token works forever, anywhereShort-lived, audience-scoped tokens; rotate; verify audience on receipt
Secrets in the promptAPI keys or tokens pasted into the system prompt or a messageNever. Prompts are logged, cached, and replayed. Inject at the transport layer, never the context
Transitive MCP trustYou trust an MCP server, so you trust everything it returns and every tool it exposesTreat MCP tool output as untrusted input; scope credentials per server; don't let one server's content drive privileged calls

The last row is where prompt injection and authorization meet. An MCP server returns content; that content can contain instructions; if your agent holds broad privilege and treats retrieved text as trusted, a malicious document becomes a command. The defense is the same least-privilege posture as Section 1: the agent should not hold the capability that the injected instruction wants to invoke.

Authorization belongs in the tool, not the prompt

Candidates reach for "add a system-prompt rule: only access records belonging to the current user." That is not an authorization control, it is a request. The model can be talked out of it, and a prompt injection routinely will be. The check must live in code the model cannot reason around, on the real arguments, using the real user's identity:

python
import anthropic
from anthropic import beta_tool

client = anthropic.Anthropic()

# The requesting user's identity is bound at request-assembly time,
# NOT supplied by the model. The model cannot forge or override it.
def make_tools(user):

    @beta_tool
    def get_invoice(invoice_id: str) -> str:
        """Fetch a single invoice by ID.

        Args:
            invoice_id: The invoice identifier to retrieve.
        """
        invoice = db.invoices.get(invoice_id)
        # Authorization is enforced HERE, in code, on the real arguments -
        # against the requesting user's grants, not the app's.
        if invoice is None or invoice.owner_id != user.id:
            # Do not leak existence. Same answer for "missing" and "forbidden".
            return "No invoice with that ID is available to you."
        return invoice.render()

    return [get_invoice]

# The agent authenticates and ACTS AS `user`, never as an omnipotent
# service account. Least privilege is enforced by what make_tools() grants.
runner = client.beta.messages.tool_runner(
    model="claude-opus-4-8",
    max_tokens=4096,
    tools=make_tools(current_user),
    messages=[{"role": "user", "content": user_question}],
)

The invariant: the model chooses which tool and what arguments; your code decides whether this user may have that result. Never the other way around.

3. Connection Protocols: MCP, API/CLI, Agent-to-Agent

The objective names three integration mechanisms and asks you to select among them. They are not interchangeable, and the exam rewards the architect who can say why one boundary fits and the others do not.

MechanismWhat it isReach for it whenWhat it costs
MCP
standardized server
A server exposing tools, resources, and prompts with discovery, over a standard protocol A capability surface is reused across many clients; N-to-M integration; a team owns a tool set others consume A protocol and a server to run and secure; transitive-trust surface
Direct API / CLI
bespoke call
Your code calls the service's own API or shells out to its CLI One bespoke integration you own end to end; you want the tightest control and the smallest surface No discovery, no reuse: every new client re-integrates by hand
Agent-to-agent
delegation
One agent delegates a task to another agent across a trust or team boundary The work crosses an org, team, or trust boundary and the far side is itself agentic A coordination protocol to agree on, and an audit trail that now spans two autonomous systems

The selection criteria that discriminate exam answers: reuse across clients (many consumers → MCP), who owns the tool (you own it and only you call it → direct API/CLI), trust boundary (crossing one → agent-to-agent, and now you own an audit problem), and discoverability (need it → MCP; don't → the discovery machinery is overhead). A single bespoke integration wrapped in an MCP server is over-engineering; a capability that ten teams re-implement by hand should have been an MCP server.

Agent-to-agent is the expensive answer, and the exam knows it. Delegating across a trust boundary means you no longer have one audit trail: you have two, and reconstructing what happened requires correlating both. Choose it only when the boundary is real. When a scenario is one team, one system, one owner, the agent-to-agent option is the distractor: it adds a coordination protocol to a problem that had no coordination.

The MCP connector: both halves are required

When you connect Claude directly to a remote MCP server from the Messages API, the classic bug is supplying only one of the two required parameters. You must declare the server in mcp_servers and reference it by name in a mcp_toolset entry inside tools. Supplying mcp_servers alone is rejected as a validation error; every declared server must be referenced by exactly one toolset. The beta header mcp-client-2025-11-20 is required.

python
import anthropic

client = anthropic.Anthropic()

response = client.beta.messages.create(
    model="claude-opus-4-8",
    max_tokens=2048,
    betas=["mcp-client-2025-11-20"],
    # HALF ONE: declare the server. Note there is NO credential here -
    # auth is injected at the transport layer, never in the prompt.
    mcp_servers=[
        {
            "type": "url",
            "name": "issue-tracker",
            "url": "https://mcp.internal.example.com/sse",
        }
    ],
    # HALF TWO: reference it by name. Omitting this - or letting the
    # names disagree - is the classic 400. Every declared server must
    # be referenced by exactly one mcp_toolset.
    tools=[
        {"type": "mcp_toolset", "mcp_server_name": "issue-tracker"}
    ],
    messages=[{"role": "user", "content": "Summarize open P1 issues."}],
)

4. Accuracy–Latency Trade-offs, and Justifying the Config

The objective is "evaluate accuracy-latency trade-offs and justify configuration decisions." The verb justify is the whole point: the exam does not want you to know that a lever exists, it wants you to state the SLO, name the lever, and name the cost. An architect who says "use caching" without saying which metric it moves and what it costs has not justified anything.

LeverMovesThe cost / the catch
Model tier (Opus → Haiku)Latency down, cost downQuality down. The trade nobody can escape
output_config.effortQuality up at higher effortLatency and tokens up. Tune per route
Adaptive thinkingQuality up on hard turnsLatency up when it engages; the model decides when
StreamingPerceived latency (time-to-first-token) downDoes not reduce total cost or total latency: only when the user first sees output
Prompt cachingCost AND latency togetherThe rare two-for-one; requires a stable prefix. When an item says "improves both," look here
BatchingCost down ~50%Latency destroyed (up to 24h window); single-turn only; results are UNORDERED: key by custom_id
Reranking depth / retrieval top-kAccuracy up with more candidatesLatency and tokens up; diminishing returns past a point
Parallel fan-outWall-clock latency down for independent workCost unchanged (same total tokens); coordination complexity up

Two rows are exam magnets. Streaming moves perceived latency only. If an item asks to lower cost or total latency, streaming is a distractor. Prompt caching moves cost and latency together: if an item asks for both, it is almost always the answer. And batching is the trap that pairs a true fact ("50% cheaper") with the wrong requirement (anything a user is waiting on).

Batch results come back in any order; key them by custom_id, never by position. Code that zips the results list against the requests list will silently mismatch every row the moment two requests finish out of order, which they will. The custom_id you set on each request is the only reliable join key, and the exam tests whether you know that.

Justifying a config is a three-part sentence. The SLO is p95 < 3s and cost < $0.02/ticket. The lever is prompt caching on the 11k-token policy corpus. The cost is a stable prefix: the corpus must sit before anything that varies, or it caches nothing. That is a defensible decision. "Use caching" is not.

5. Observability at Scale

The objective is "analyze observability challenges and select monitoring strategies at scale." The word scale is doing the work: at ten requests a day you can log everything; at ten million you cannot, and the exam is testing whether you know what to keep and what to drop.

What to emit

SignalWhy it matters
Trace / span per turn, tool call, retrievalLocalizes where a slow or wrong response was born: model, tool, or retrieval
request_id on every responseresponse._request_id is public despite the underscore; it is what Anthropic needs to trace a failure. Log it always
Token usage, split outcache_read_input_tokens vs input_tokens: see the trap below
Stop reasonsend_turn, max_tokens, tool_use, refusal: a spike in max_tokens means truncation; a spike in refusal means a content or prompt problem
Tool error ratesA tool failing 5% of calls is invisible in aggregate latency but corrodes correctness
Per-tenant costThe number the CFO asks for, and the only way to catch one tenant's runaway loop
input_tokens excludes cache reads, so naive cost math undercounts, and naive savings math over-counts. Total prompt size is input_tokens + cache_creation_input_tokens + cache_read_input_tokens. The input_tokens field is the uncached remainder only. An agent that ran for an hour but shows input_tokens of 4k did not process 4k tokens: the rest was served from cache. Sum the three fields, or your dashboard lies in whichever direction is most flattering.

Emitting the trace record is the concrete skill. The usage split is not optional detail, it is the difference between a cost dashboard that reconciles and one that does not:

python
import json, logging

logger = logging.getLogger("agent.trace")

def emit_trace(response, tenant_id, turn_id):
    u = response.usage
    record = {
        "turn_id": turn_id,
        "tenant_id": tenant_id,
        # Public despite the underscore - this is what Anthropic traces on.
        "request_id": response._request_id,
        "model": response.model,
        "stop_reason": response.stop_reason,
        # Split the usage out. input_tokens is the UNCACHED remainder only -
        # summing all three gives the true prompt size and the true cost.
        "input_tokens": u.input_tokens,
        "cache_read_input_tokens": u.cache_read_input_tokens,
        "cache_creation_input_tokens": u.cache_creation_input_tokens,
        "output_tokens": u.output_tokens,
    }
    # A metric (cheap, aggregate) - NOT the full prompt (PII + volume).
    logger.info(json.dumps(record))

The scale problem, and the three tiers of telemetry

You cannot log full prompts and responses for every request: that is PII you now have to protect and a volume you cannot afford to store or search. So you don't. You sample traces, redact, aggregate the metrics, and keep exemplars of the interesting cases. The discipline is knowing which tier each question belongs to:

TierCostCoverageAnswers
MetricsCheapEvery request, aggregatedIs it up? Is it fast? Is it affordable? (and alerting)
TracesExpensiveSampled + redacted, plus exemplarsWhy did this request behave this way? (debugging)
EvalsOfflineLabelled dataset, run deliberatelyIs it correct? (Lab 6)

Metrics tell you something broke. Traces tell you where. Evals tell you whether the fix actually improved correctness. Confusing the tiers (trying to answer "is it correct?" from production metrics, or "is it up?" from an offline eval set) is a recurring wrong answer in this domain.

6. The Three Questions Observability Must Answer

Strip everything above to its intent and observability exists to answer three questions, in order:

QuestionSignal that answers itWho usually instruments it
Is it up?Error rate, latency, availability metricsEveryone. This is table stakes
Is it correct?Evals, tool error rates, human-override signals, refusal spikesAlmost no one, and it is the one that matters most
Is it affordable?Per-tenant token cost, cache-hit ratio, model mixFinance notices when it's too late
Most teams instrument "is it up?" and are blind to "is it correct?" A Claude system can be 100% available, sub-second, on budget, and quietly wrong 15% of the time, because none of the up-time signals see correctness. Correctness is invisible to infrastructure monitoring by construction; it requires evals (Lab 6) and captured override signals. When an item describes a system that "passes all health checks but users complain," the gap is the second question, and the answer is never another latency dashboard.

7. Design Exercise

Self-driven lab Lab5_Self_Driven_Lab.ipynb

Objective: harden a multi-tenant agent platform against the four failure modes this lab covered: bloat, broken authorization, wrong protocol, and blind observability. About 60 minutes.

Scenario. You run a multi-tenant agent platform. Each tenant's agent needs three integrations: read from the tenant's CRM, write to the tenant's calendar, and issue invoices against the tenant's billing account. Today every tenant's agent runs as one shared service account, holds all three capabilities plus a dozen more inherited from a template, authenticates once at session start, and logs nothing but HTTP status codes. Volume is growing past what full-prompt logging can sustain.

  1. Run a capability-bloat audit. List every tool on the agent. For each, name the tenant task it serves. Delete the ones you cannot name, and justify each deletion as least privilege, not cleanup. Which of the twelve inherited tools survive? Write the sentence that kills the rest.
  2. Design per-user authorization. The shared service account is a confused deputy: any tenant's agent can reach any tenant's data. Redesign so each agent acts with the requesting tenant's permissions, and each of the three tool calls is authorized against that tenant's grants on the real arguments, in code, not in the prompt. Where exactly does the check live for the invoice tool?
  3. Choose a protocol per integration. CRM read, calendar write, invoice issuance: pick MCP, direct API/CLI, or agent-to-agent for each, and defend it with the four criteria (reuse, ownership, trust boundary, discoverability). Which one, if any, actually crosses a trust boundary? Which is a bespoke integration that an MCP server would over-engineer?
  4. Reconcile the SLO. Invoice issuance must feel instant to the user but need not be cheap; the nightly CRM sync must be cheap but no one is waiting on it. Assign a lever to each, and name the one you would not use for the interactive path and why (hint: it is 50% cheaper and completely wrong here).
  5. Specify the trace schema. Write the exact fields you emit per turn: include request_id, the three-way token split, stop reason, per-tenant cost, and tool error flag. State which of the three questions (up / correct / affordable) each field serves.
  6. Specify the sampling policy. You cannot log full prompts at this volume. Define what you sample, what you redact, what you aggregate, and which exemplars you always keep (hint: keep every refusal and every tool error, sample the rest). State what you will be unable to answer as a result, and why that is an acceptable trade.

Step 2 is the one the exam tests hardest. Step 6 is the one that keeps the platform legal.

Check Yourself

Exam-style items. Commit to an answer before expanding.

An agent for an internal analytics tool exposes 18 tools. Users complain it frequently calls the wrong one: running a report when they asked to export data, and vice versa. Availability and latency are fine. Which change most directly improves correctness?
  • A. Upgrade to a more capable model tier so it chooses better.
  • B. Add a system-prompt rule listing which tool to use for which request.
  • C. Reduce the tool surface to the few tools users actually invoke, removing the rest.
  • D. Add logging on every tool call so the wrong choices can be reviewed.

Correct: C. More tools degrade tool-selection accuracy: every exposed tool is another option competing for the model's attention on every turn. Eighteen tools where users touch a handful is a bloated surface; pruning it to the tools that serve real tasks makes the model measurably better at choosing among what remains. Bloat is a correctness problem, not only a security one.

A treats a selection problem as a capability problem: a smarter model still has to disambiguate 18 options, and the guide's default answer is "less," not "more model." B is a request the model can reason around, and it grows the prompt that is already overloaded. D is detective: it tells you the agent chose wrong after it did, which is how you diagnose the surface you should have pruned, not how you fix it.

A support agent authenticates as a single service account that holds read access to every customer's tickets. Per-customer scoping is enforced by a system-prompt instruction: "only discuss tickets belonging to the authenticated user." A security review flags this. What is the correct characterization, and the fix?
  • A. It is fine: the prompt instruction constitutes the authorization control.
  • B. It is a confused deputy; enforce per-user authorization in code on each tool call, against the requesting user's grants.
  • C. It is fine once you add logging of cross-customer access attempts.
  • D. Rotate the service account credentials on a shorter schedule.

Correct: B. The agent authenticates as an omnipotent service account and lets the model, via a prompt instruction, decide who sees what. That is the textbook confused deputy: the identity layer authorizes nothing, so any prompt injection or model slip crosses customer boundaries. The rule is that the agent must act with the permissions of the requesting user, and the check must live in code on the real arguments, not in the prompt.

A mistakes a request for a control: a prompt instruction is precisely what an injection overrides. C is detective: logging tells you a boundary was crossed after the data leaked. D addresses credential hygiene, which is real but orthogonal: a freshly rotated omnipotent token is still omnipotent, and the authorization gap is untouched.

A team wants two things from its Claude ticketing agent: lower cost per ticket, and a faster time-to-first-response for the user. Each request sends an 11,000-token policy corpus followed by a short question. Which two changes serve these goals? (Choose two.)
  • A. Place the policy corpus at the start of the system prompt and enable prompt caching.
  • B. Move the workload to the Batches API for the 50% discount.
  • C. Stream the response so the user sees tokens as they are generated.
  • D. Set output_config.effort to max so answers are right the first time.
  • E. Truncate the policy corpus to its first 2,000 tokens.

Correct: A and C. Caching a stable 11k-token prefix moves cost and latency together: reads bill at roughly a tenth of input, and the prefix is not reprocessed, cutting time-to-first-token. That is the rare two-for-one lever. Streaming does not reduce cost or total latency, but the stated goal is a faster first response, and streaming is exactly what lowers perceived time-to-first-token.

B halves cost but destroys latency: batches run in a window up to 24 hours; no user is waiting on that. It is the trap: a true fact ("50% cheaper") attached to the wrong requirement. D raises quality at the cost of both latency and tokens, the opposite of both goals. E cuts cost by discarding policy the agent needs to answer correctly: that is not optimization, it is changing the product.

A Claude platform passes every infrastructure health check (99.98% uptime, sub-second p95, on budget), yet a growing number of users report that answers are subtly wrong. The team keeps adding latency and error-rate dashboards and finds nothing. What is the gap?
  • A. The metrics pipeline is dropping error events; fix the metrics.
  • B. They are instrumenting "is it up?" but not "is it correct?" Correctness needs evals and captured override signals, not more infrastructure dashboards.
  • C. p95 latency is hiding a p99 tail; add tail-latency monitoring.
  • D. The model tier is too low; upgrade it.

Correct: B. Observability answers three questions: is it up, is it correct, is it affordable. Uptime, latency, and cost all answer the first and third; none of them can see correctness, which is invisible to infrastructure monitoring by construction. A system that is fast, available, and quietly wrong 15% of the time is the canonical symptom. Correctness requires offline evals (Lab 6), tool error rates, refusal spikes, and human-override signals, not another latency chart.

A and C both propose more of the same up-time instrumentation, which by definition cannot surface a correctness defect. D might change quality, but with no eval you have no way to know whether it helped, hurt, or did nothing: you would be guessing, which is precisely the blindness the item describes.

Key Takeaways

  • Least privilege removes the capability; it does not guard it. When one option deletes a tool and the others log, prompt, or upgrade around it, deleting wins: the guide's own Sample 1 deletes two tools.
  • Capability bloat is a correctness problem, not only a security one: more tools measurably degrade tool-selection accuracy. Fewer, sharper tools is often the reliability fix.
  • The agent must act with the permissions of the requesting user, not the application. A shared omnipotent service account is a confused deputy; authorize each tool call in code, on the real arguments, against that user's grants.
  • Authorization belongs in the tool implementation, never the prompt. A prompt rule is a request an injection overrides; a code check is a control it cannot.
  • Protocol selection: MCP for reusable, discoverable, N-to-M surfaces; direct API/CLI for a single bespoke integration you own; agent-to-agent only when a real trust boundary is crossed, and now you own two audit trails. The MCP connector needs both mcp_servers and mcp_toolset.
  • Justify configs as SLO + lever + cost. Streaming moves perceived latency only; caching moves cost and latency together; batching is 50% cheaper, latency-destroying, and returns results UNORDERED: key by custom_id.
  • input_tokens is the uncached remainder only: sum it with cache-read and cache-creation tokens or your cost math is wrong. Always log response._request_id (public despite the underscore).
  • At scale you cannot log everything: sample traces, redact, aggregate metrics, keep exemplars. Distinguish metrics (cheap, aggregate, alerting) from traces (expensive, sampled, debugging) from evals (offline, labelled, correctness). Most teams answer "is it up?" and are blind to "is it correct?"