Lesson 8: Leave What You Don't Know Open
Training overview · Lesson 8 of 11 · Previous: Record a compensating control · Next: Follow a credential
Goal
An unanswered fact comes back as a question. A guess makes the question go away without settling anything.
After this lesson you can tell an open question from a settled one, answer only what you were told, and recognise what a guess would do before you make it.
What you start from
Import L8-start.zip.
It is the lesson 7 solution plus a second agent, the Ticket summariser, already drawn. The summariser reads each ticket, looks the customer up in the CRM, and writes its summary only to the staff console. Nobody has answered any of its facts yet.
Before you import, create the lesson 7 control. The starting model refers to the control Support copilot: untrusted-content isolation by name, and the import does not create it. If you did not do lesson 7, create it in the Browser first, with exactly that name and the answers from lesson 7. Without it, the import reports a warning starting "Could not find control: Support copilot: untrusted-content isolation" and the copilot's trifecta shows no control credit. The summariser, which this lesson is about, is not affected.
If you continue your own model from lesson 7, draw the summariser first:
- Add the boundary Summariser container (class Container), inside Support network.
- Add Ticket summariser (class Agent Service), inside Summariser container.
- Draw Context Ingress: ticket (class Context Ingress) from Ticket system to Ticket summariser, and tick Ticket text on its Data tab.
- Draw CRM lookup (class Agent Tool Call) from Ticket summariser to CRM, and tick CRM API key on its Data tab.
- Draw Context Ingress: customer records (class Context Ingress) from CRM to Ticket summariser, and tick Customer records on its Data tab.
- Draw Summary to staff (class HTTP/S Request) from Ticket summariser to Staff console.
- Draw Inference Request / Response (class Inference Request / Response) from Ticket summariser to Hosted model.
- Attach CRM API key to Summariser container.
Drawing it repeats lesson 1. The new work in this lesson is reading the questions and deciding which ones you can answer.
The recipe step
Step 6: Leave what you do not know open
Write a value only when someone who knows has stated it, or you found it in evidence you can point to. Leave every other fact unanswered, on purpose. This holds for both answers: guessing that a tool exists is as much a guess as guessing that it does not.
Why: An unanswered fact comes back as a question that names what would settle it. A guess makes the question go away without settling anything.
What a guess does:
| If you guess | What happens |
|---|---|
| Answers on components | |
consequential_tools_all_require_approval = true (Agent Service) | The agent's actions read as approved through its own answer, so injection to action is no longer raised through them, unless another answer says actions run unapproved: approval_gate_present = false on the agent, a chat frontend or a source that approves automatically, or a drawn path answered unapproved. With consequential tools answered false, the answer covers nothing the agent runs and does not count. |
consequential_tools_present = false (Agent Service) | With code execution also answered false, and no MCP server it calls answering destructive tools true, the agent reads as holding no tools that act: the question about them closes, and injection to action cannot be raised through its own tools. With code execution open, the question stays; with code execution true, or such a server, it is raised as before. The agent's own approval answer then counts only toward actions running unapproved. |
code_execution_tools_present = false (Agent Service) | The agent reads as running no code: with consequential tools also false, injection to action is no longer raised through its own tools. Whether it reads files stays open: the question moves to the harness that might read them for it. |
network_egress_default_deny_allowlist = true (Agent Service) | The agent's own undrawn ways out read as closed. A drawn flow from it that reaches an outsider still counts. |
harness_approval_mode = default or plan (Agent Harness) | Every agent the harness runs reads as waiting for approval, so injection to action is no longer raised for them through the harness, unless the agent's own approval answer, or dangerously_skip_permissions_in_use on the harness, says otherwise. |
markdown_image_rendering_policy = disabled or allowlisted_trusted_domains_only (Agent Chat Frontend) | Every undrawn way out through images rendered in the chat frontend reads as closed. A drawn flow from it still counts. |
| Answers on data items | |
| Sensitivity = Public (data item) | With Credential = No, the item no longer counts as private, and an element that handles only it no longer counts as a sensitive target or as something a credential unlocks. |
| Credential = No (data item) | The item is no longer treated as a credential: no credential chain starts from it. An item whose class is a credential class counts as one while the answer is open; another item counts only once it is answered Yes. |
| Origin = Authenticated first party (data item) | The item no longer counts as untrusted, unless the flow carrying it comes from an Untrusted zone or an untrusted source class. |
| Origin = Curated internal (data item) | The item counts as trusted, as for first party. For a knowledge base fed from outside, the knowledge route loses it, unless the ingestion is drawn: a pipeline's Knowledge Write carries the least trusted origin it ingests, whatever origin is stated. |
| Origin = Agent relayed (data item) | The item counts as trusted on the agent's own routes, so injection to action loses it as untrusted input. If the relaying agent and its hand-off are drawn, the relayed route still carries the relayer's untrusted input to the trifecta. |
| Origin = Model generated (data item) | Content the model wrote counts as trusted on the agent's own routes, even when it wrote it from untrusted content: the taint is laundered there. Read back from the agent's own memory it reads undetermined instead, and Facts to establish says second-order poisoning is not analysed. Through a store another agent reads, the writer's untrusted input still reaches the reader when the same item is drawn on the write and on the read. |
| Answers on paths | |
human_approval_for_sensitive_tool_calls = true (Agent Tool Call) | That path reads as approved. Injection to action is no longer raised only when every path into and out of the agent is approved, untrusted content reaches it no other way (a tool server, a knowledge item), and it runs no code off its drawn calls. |
human_approval_for_actions_after_untrusted_ingress = true, or untrusted_ingress_can_trigger_consequential_actions = false (Context Ingress) | As for an approved tool call, for the path the untrusted content comes in on. |
| Zones and assertions | |
| Assert outbound flows complete (on an element) | Every undrawn way out of that element reads as closed; its drawn flows are still followed. It is the user's answer to give, never a shortcut. |
| Zone = Internal for a boundary drawn Untrusted (Open internet) or Public (Internet-facing) | Content from it no longer counts as untrusted by zone, and what reaches it no longer counts as a way out: untrusted input and the trifecta's way out can both fall, and a public-facing foothold goes. |
| Zone = Internal for a boundary drawn Vendor (Trusted external) | What reaches the vendor reads as staying inside: the analysis stops asking whether an untrusted party can read it. |
The table above is the reason for this step. Each row is a value that looks safe and, if nobody told you it is true, can remove a finding for good. The polarity differs from fact to fact: "no" looks safe for a fact about what tools exist, "yes" for a fact about approval or egress. So the rule is not "never write a false value". It is "never answer what nobody told you".
Steps
Read before you answer.
- Open the model, create a Threat Patterns analysis, open it with View results and click Generate (see Run Threat Patterns on a lesson model).
- On the Findings tab, find the lethal trifecta at the ticket summariser.
- Open the Facts to establish tab and read every question about the summariser. Do not change anything yet.
Compare what you see with Before you answer below.
Ask, then answer only what you were told. You ask the team that runs the summariser. They tell you:
- The summariser only writes summaries for staff to read. It posts nothing, updates nothing and sends nothing on its own.
- It runs no code.
They do not know whether the staff console renders images in the summaries, and they cannot say who at the CRM vendor can read what the summariser sends there. Leave those facts open.
Record the two answers you were given:
- On Ticket summariser, set:
- Consequential Tools Present (
consequential_tools_present, under Human Oversight) =false - Code Execution Tools Present (
code_execution_tools_present, under Application Security) =false
- Consequential Tools Present (
Generate again.
What you should see
Before you answer
Findings: 2 systemic findings: 1 Confirmed · 1 Not ruled out.
| Finding | State | Legs |
|---|---|---|
| Lethal trifecta at Support copilot | Confirmed · control modelled on U, X | U present · P present · X present · W absent (aggravating) |
| Lethal trifecta at Ticket summariser | Not ruled out · unmitigated | U present · P present · X undetermined · W undetermined (aggravating) |
Facts to establish: 7 questions.
Open questions on Not ruled out findings:
- Lethal trifecta at Ticket summariser:
- Can an untrusted party read what reaches CRM?
- Draw the outbound flows of Staff console, or answer Markdown Image Rendering Policy (
markdown_image_rendering_policy, in Output Rendering Exfiltration) on Staff console - Every drawn route from CRM ends inside; assert its outbound flows complete if nothing else leaves it
Facts that could raise a finding:
- Set the attribute Code Execution Tools Present (
code_execution_tools_present, in Application Security) on Ticket summariser, or draw its Agent Tool Call to the harness - Draw the Agent Tool Call from Support copilot to the harness that reads files for it, if there is one
- Set the attribute Consequential Tools Present (
consequential_tools_present, in Human Oversight) or Code Execution Tools Present (code_execution_tools_present, in Application Security) on Ticket summariser - Draw the runtime boundary or the Agent Tool Call that ties Ticket summariser to its harness
Model gaps: 1 of 2 agents have no hosting harness drawn, so their W cannot be read from one (on Ticket summariser)
The support copilot's trifecta is unchanged from lesson 7. The summariser has a lethal trifecta, Not ruled out: U and P are present, X is undetermined. Injection to action at the summariser is short of a finding, and its questions sit under Facts that could raise a finding. So does a question about the CRM API key, which the summariser's container now holds too: the same question the copilot had in lesson 2. The copilot's own key question reads differently, because you answered its code fact in lesson 5.
The model gaps sit at the top of the Facts to establish tab. The line about agents with no hosting harness drawn now counts the summariser: it reads "1 of 2 agents have no hosting harness drawn, so their W cannot be read from one", and the line beneath it names the element, "on Ticket summariser".
After you answer
Findings: 2 systemic findings: 1 Confirmed · 1 Not ruled out.
| Finding | State | Legs |
|---|---|---|
| Lethal trifecta at Support copilot | Confirmed · control modelled on U, X | U present · P present · X present · W absent (aggravating) |
| Lethal trifecta at Ticket summariser | Not ruled out · unmitigated | U present · P present · X undetermined · W absent (aggravating) |
Facts to establish: 5 questions.
Open questions on Not ruled out findings:
- Lethal trifecta at Ticket summariser:
- Can an untrusted party read what reaches CRM?
- Draw the outbound flows of Staff console, or answer Markdown Image Rendering Policy (
markdown_image_rendering_policy, in Output Rendering Exfiltration) on Staff console - Every drawn route from CRM ends inside; assert its outbound flows complete if nothing else leaves it
Facts that could raise a finding:
- Draw the Agent Tool Call from Support copilot to the harness that reads files for it, if there is one
- Draw the Agent Tool Call from Ticket summariser to the harness that reads files for it, if there is one
Model gaps: No gaps these patterns can detect; not an attestation of completeness.
The questions about the summariser's tools and the one tying it to a harness are gone, and so is the model gap. The summariser's trifecta is still Not ruled out, with the same questions on its X leg. The CRM API key's question stays, in the new words the copilot's took in lesson 5: Draw the Agent Tool Call from Ticket summariser to the harness that reads files for it, if there is one. You said the summariser runs no code, so that fact is not asked again; only a drawn call to a harness could show that something reads its files for it.
What changed and why
The tool questions were settled. You answered both of the summariser's action-tool facts. It has no tools that act and runs no code, so injection to action cannot be raised for it: the question about it is answered, not hidden. Its approval no longer matters either, so the harness question goes too. The trifecta's W leg reads absent: an agent with no consequential tools does not write without approval.
The X questions stay, and that is correct. The trifecta asks, under Open questions on Not ruled out findings:
- Can an untrusted party read what reaches CRM? A vendor destination alone does not answer it. No attribute or drawing in this model settles it yet, so it stays open.
- Draw the outbound flows of Staff console, or answer Markdown Image Rendering Policy (
markdown_image_rendering_policy, in Output Rendering Exfiltration) on Staff console. A chat frontend that renders images can send data out through the image addresses. Nobody told you how this console behaves, so leave it open. - Every drawn route from CRM ends inside; assert its outbound flows complete if nothing else leaves it. The routes drawn from the CRM lead only back to the two agents. Whether anything else leaves it is partly your own configuration. In a SaaS CRM, your own administrators set up the connected apps, webhooks and outbound workflows that send its data elsewhere. Assert the CRM's outbound flows complete only when the CRM administrator confirms that nothing else leaves. In 2025, attackers used tokens stolen from one such connected app, the Salesloft Drift chat integration, to pull data out of many companies' Salesforce instances.
Who settles these questions
Do not guess. Find the person who knows, and give each open question an owner and a date. Until it is settled, treat an open way out as present when you plan monitoring.
| Question | Who settles it | How |
|---|---|---|
| Does the staff console render images or links in model output? | The owner of the staff console | Read its rendering settings, then test them: put a markdown image that points at an address you control into a test ticket, and see whether the console fetches it |
| What does the CRM pass on, and who can read what reaches it? | The CRM administrator, for connected apps, webhooks and outbound workflows. Vendor management, for the vendor's subprocessor list and audit report | Ask for the list of integrations and outbound rules; ask vendor management for the documents |
| Is an agent's network egress limited to an allowlist? | The platform or network team | Read the egress configuration itself, and the list it allows. Do not settle it by asking someone's opinion |
What the patterns do not check: people
The summariser writes for people. A summary can steer a person even when the agent itself can do nothing: an injected ticket can make the summary show a fake security alert with a phone number to call. The patterns do not check this. Note it as an item for manual review: what does the console show staff, and what would they do with it?
Not ruled out is not a lower grade of risk. The summariser reads untrusted tickets and confidential customer records. Whether it can send them to an outsider is the open part. The finding says exactly what is missing. Answering a question to make it go away would leave the summariser no safer and the model wrong.
Exercise: The guess
On a scratch copy of the starting model, before asking anyone, make a guess. Assume the summariser posts its summaries itself and that a person approves every post. Set both facts without anyone saying they are true. Choose Yes for Consequential Tools Present first: All Consequential Action Paths Require Approval appears only after that.
On a scratch copy: import L8-start.zip and rename it.
- On Ticket summariser, set:
- Consequential Tools Present (
consequential_tools_present, under Human Oversight) =true - All Consequential Action Paths Require Approval (
consequential_tools_all_require_approval, under Human Oversight) =true
- Consequential Tools Present (
Generate again.
Findings: 2 systemic findings: 1 Confirmed · 1 Not ruled out.
| Finding | State | Legs |
|---|---|---|
| Lethal trifecta at Support copilot | Confirmed · control modelled on U, X | U present · P present · X present · W absent (aggravating) |
| Lethal trifecta at Ticket summariser | Not ruled out · unmitigated | U present · P present · X undetermined · W absent (aggravating) |
Facts to establish: 5 questions.
Open questions on Not ruled out findings:
- Lethal trifecta at Ticket summariser:
- Can an untrusted party read what reaches CRM?
- Draw the outbound flows of Staff console, or answer Markdown Image Rendering Policy (
markdown_image_rendering_policy, in Output Rendering Exfiltration) on Staff console - Every drawn route from CRM ends inside; assert its outbound flows complete if nothing else leaves it
Facts that could raise a finding:
- Set the attribute Code Execution Tools Present (
code_execution_tools_present, in Application Security) on Ticket summariser, or draw its Agent Tool Call to the harness - Draw the Agent Tool Call from Support copilot to the harness that reads files for it, if there is one
Model gaps: No gaps these patterns can detect; not an attestation of completeness.
Compare it with After you answer, the result of the truthful answer.
- The screen matches the truthful answer. There is no injection to action at the summariser and no model gap. The question about its action tools and the one tying it to a harness are gone. The summariser's trifecta shows W as absent, just as it does after the truthful answer. One line differs: the CRM API key's question on the summariser still asks for Code Execution Tools Present (
code_execution_tools_present, in Application Security), because the guess left the code fact unanswered. That line says nothing about the guess itself. - Two guesses gave the same result as two facts. The truthful answer said the summariser has no tools that act. The guess says it has them and that a person approves every one. Both make injection to action impossible to raise, and nothing about injection to action on screen tells them apart.
- The guess also says something false about the system. Anyone who reads the model later learns that the summariser posts on its own behind an approval step. Neither is true.
That is the danger: a guess and the truth can look the same on screen. In lesson 6 the approval answer was a fix, because it was true. Here it is a cover-up. The analysis cannot tell the two apart: it believes what the model says.
Delete the copy when you are done.
Next
Lesson 9: Follow a credential. The copilot gains a code tool, and the CRM API key it has held since lesson 2 becomes a credential chain.
