Illumio Agentic AI Risk Assessment
Answer four questions a CISO or auditor asks about agentic AI, with evidence
from the PCE:
- Which managed workloads run agentic AI clients (Claude, ChatGPT, Cursor,
Copilot, Codex and the like)?
- What internal systems do those agents connect to, on which ports, under
which user?
- What do the agents send outbound, and to whom?
- Does current policy stop any of it, and what should change?
Output: a written risk assessment (risk register, regulatory mapping,
prioritised Illumio recommendations) and an interactive Sankey of endpoint to
agent to internal application.
The skill is descriptive and advisory. It never provisions policy. It may
draft rulesets and hand them to the user for review.
Files
| File |
Use it for |
references/agent-catalog.md |
process names, matching rules, vendor egress hints |
references/risk-rubric.md |
likelihood and impact scales, R1 to R11, regulatory table |
references/report-template.md |
ten-section skeleton with the sentence each section opens with |
references/sankey-template.html |
the reach diagram; replace RAW, HOSTS, APPS |
scripts/aggregate_flows.py |
merges saved query results into one flow table, emits the Sankey constants |
Read the catalog and rubric before Step 3. Read the template before Step 7.
Preconditions and the one up-front question
illumio-mcp:check-pce-connection must succeed. If it fails, stop and say
so.
- Ask one question before starting, covering both: report format (Claude
Doc, .docx, markdown in chat) and lookback window (default: everything the
PCE holds, typically 4 to 30 days). Ask nothing else up front. Make
assumptions and state them at the top of the report.
- Optional scope override: a label expression for the source population.
Default
type=endpoint. When type is not a label dimension, fall back in
this order: app=vdi plus app=laptop (two values, so two queries per
slice and --population "app=vdi|app=laptop" for the script), then
env=Users. When several of these exist, type wins. If no endpoint-like
label exists, use all workloads and say so in the scope section.
Workflow
Create a task list with one item per step plus the verification task. Save
every raw tool result to a scratch file; the harness writes large results to
disk anyway, and the script reads them from there.
Step 1: Connection and label schema
illumio-mcp:check-pce-connection
illumio-mcp:get-labels max_results: 500
get-labels returns a Python-repr list (Labels: [{'href':..., 'key':..., 'value':...}, ...]), not JSON. Build a map of label key to values. Decide two things and write them down:
- the source-population key (
type, else app, else env)
- the defining label set (see Label handling below)
Step 2: Workload inventory with labels
illumio-mcp:get-workloads detail_level: "compact", max_results: 10000
Save the result as scratch/workloads.json. compact returns split-format
JSON (columns + data) with href, hostname, ip_addresses, os_type,
enforcement_mode and one column per label key. It is a superset of
labels_only and also gives the IPs needed to resolve IP-only destinations
and the per-workload enforcement mode that Step 5 needs (the enforcement
status tool only returns aggregates). Build
hosts[hostname] = {href, ips, enforcement_mode, labels{key: value}} for
every workload; destinations are resolved against it later. Select the source population by
the chosen label. Record the count and the split by the second-level grouping
(VDI versus laptop from app, or whatever the org uses).
Step 3: Traffic from the source population, grouped by process
illumio-mcp:get-traffic-flows
group_by: ["process", "source", "destination", "port", "proto", "policy"]
include_sources: ["<population label>", "<slice label>"]
include_destinations: ["<destination slice label>"]
start_date: "YYYY-MM-DD" end_date: "YYYY-MM-DD"
The result is split-format JSON: columns plus data rows, with
process_name (full path as the VEN reported it), src_hostname, src_ip,
dst_hostname, dst_ip, dst_fqdn, port, proto, policy_decision and
num_connections, sorted by connections descending. total_pce_flows is the
number of flows the PCE returned before grouping.
Two constraints decide the query plan:
- The PCE returns at most 500 flows per query, silently. The server
overrides
max_results to 500, and truncated only reports byte-size
trimming of the response, never the PCE cap. A result with
total_pce_flows of 500 (or 500 rows) is a truncated result. Split until
every slice comes back under 500.
include_sources and include_destinations take label shorthand only
(key=value). Workload hrefs and bare IPs fail with a
TrafficQueryFilter type error. Entries in one list are ANDed:
["bu=hr", "bu=health"] returns nothing unless a workload carries both.
Slicing plan:
- Outer loop: values of a source-side label that partitions the population
(
role works: contractor, admin, employee; os as a fallback). When the
population itself is two label values (app=vdi, app=laptop), those two
values are the outermost loop and role nests inside.
- OR is always a second query. For
bu=hr or bu=health, run one query
with ["bu=hr"] and one with ["bu=health"]; the script merges them.
- Inner loop: values of a destination-side label that partitions internal
systems (
bu; app if there is no bu).
- One extra query per source slice with no destination filter, to catch
egress and unlabeled destinations.
- Save each result as
flows_<src>_<dst>.json in the scratchpad. Note the
row count of every slice; any slice at 500 must be split again (add a third
label, or split by proto, or halve the date window).
Merge:
python3 scripts/aggregate_flows.py \
--flows scratch/flows_*.json \
--workloads scratch/workloads.json \
--egress scratch/egress.json \
--population "type=endpoint" \
--out scratch/merged
The script de-duplicates on (process, source, destination, port, proto), tags
each row with the catalog family, extracts the Windows user from the process
path, resolves destinations against the workload map, and splits rows into
internal (destination is a managed workload), egress (IP or FQDN not
in the map), ambiguous (host process) and unattributed (no process).
It also prints every input file that held exactly 500 rows. Run --help for
options. Outputs: merged/flows.csv, merged/summary.json,
merged/sankey_data.js holding the RAW, HOSTS and APPS constants, and
merged/sankey.html, the template with those constants spliced in.
Step 4: Egress detail
illumio-mcp:discover-process-egress
include_sources: ["<population label>"]
process: ["claude", "chatgpt", "cursor", "codex", "copilot", "gemini", "windsurf", "ollama"]
lookback_days: 30
limit: 100
max_results: 500
- Map destinations to providers with the hints table in the catalog. Confirm
with the FQDN the PCE resolved or reverse DNS; otherwise write "likely".
- Save the result as
scratch/egress.json and pass it to the script with
--egress. Each finding carries process (basename only), destination
(FQDN if resolved, else IP), port, proto, policy_decision,
permitted_today, connections, and likely_provider with
provider_confidence when the server could attribute it. Record
permitted_today and connection counts per destination.
- Any RFC1918 destination not in the workload map is an unmanaged internal
host. Report it separately from vendor egress. Cross-check whether the
same address appears as a source of blocked or potentially blocked
traffic against the endpoints; if so, call it a possible foothold (R7).
Step 5: Policy posture
illumio-mcp:get-workload-enforcement-status
illumio-mcp:get-rulesets max_results: 200
illumio-mcp:compare-draft-active resource_type: "rule_sets"
illumio-mcp:enforcement-readiness (optional, for the two or three most-reached apps)
- Enforcement mode counts overall (from the enforcement status tool) and
for the source population (from the
enforcement_mode column of the
Step 2 inventory; the status tool only returns per-app aggregates).
- For each ruleset decide whether it touches the population as consumer or
any agent-reached app as provider. Note rules whose name or description
claims process restriction but whose services are port-only (R5). Note
any/any allows inside app scopes.
- From the draft-versus-active result, say whether the observed state is
live or partly unprovisioned. Report only what the filtered call returns;
pending changes on other object types are out of scope.
- In selective enforcement, "allowed" in Explorer means "no deny matched",
not "an allow rule exists". Say so in the posture section.
Step 6: Analysis
Compute from merged/flows.csv and summary.json:
- endpoints with agents, by kind and role; agents per endpoint; endpoints
per agent
- connections and distinct flows per (agent, app) and per (agent, host, port)
- regulated reach: destination carries
compliance=*, env=PCI, or the
org's equivalent
- administrative reach: destination
role in {dc, jumpbox, bastion,
admin} or app in {ad, jump-infra}, and port in {22, 3389, 88, 389, 445,
5985, 5986}
- odd ports: anything on a regulated or admin host that is not the app's
expected service (TFTP 69, POP3 110, IMAP 143, 5938 and similar are red
flags)
- personal-data reach: apps named hr, hrm, crm, payroll, people, or
carrying a
data=pii style label
- label contradictions: if a
risk or tier label exists, compare it to
observed reach (R10)
Step 7: Score and write
Fill the risk register and regulatory table from references/risk-rubric.md
and write the report along references/report-template.md. Publish
merged/sankey.html as the Sankey artifact (favicon, title "Agentic AI
Reach"). Then run the verification task below before publishing anything.
Label handling
Every workload in the report, source or destination, is shown with its
defining labels: the ones that say what the thing is, not all labels.
- Always show when present:
app, env, role, loc.
- Show when they add meaning:
bu, type, os, compliance, data or
classification.
- Drop: labels that only steer policy or incident workflow.
quarantine.illumio.com=*, DFIRBubble=*, IR bubble labels, risk=* when
it is a policy knob, kc=* cluster tags, temporary migration tags.
Rule of thumb: if the label would change during an incident or a policy
rollout without the asset changing, it is a policy label and is dropped.
Dropped means dropped from the printed label set only. risk and tier are
still read for the R10 comparison below.
Decide the set once in Step 1, state it in the scope section, and use the
same set in every table. In the Sankey the destination column shows app
with bu and compliance as small print; the source column shows hostname
with role and user.
If a risk label exists, compare it to the evidence and report
contradictions. VDIs labelled Low Risk that carry the most agent reach is a
finding, not decoration.
Outputs
Report: the ten sections of references/report-template.md, in order,
each opening with its point. Format follows the up-front answer: Claude Doc
by default in Cowork, a markdown file in Claude Code, .docx on request via
the docx skill.
Sankey: three columns (endpoint grouped VDI then laptop, agent process,
internal application), link width is connections, colour by agent in the
fixed order Cursor, ChatGPT, Claude, then others. Filters for agent, source
kind and regulated only. Tooltip lists the hosts and ports behind a ribbon.
Sortable flow table below. Light and dark theme. d3 7.9.0 and d3-sankey
0.12.3 from cdnjs. The script fills RAW, HOSTS and APPS; leave the
rest of the template alone.
Draft rulesets (only when the user asks "write the rules"): emit ruleset
drafts as JSON or as create-ruleset / create-deny-rule call plans. Never
provision. Patterns:
- deny
type=endpoint to compliance=SWIFT on all services
- process-qualified allow (
chrome.exe, msedge.exe on 443) paired with a
deny on all services, for browser-only apps
- deny agent process services (
Cursor.exe, Claude.exe, ChatGPT.exe and
the macOS paths) to role=dc on 88, 389, 445 and to app=hrm on all
- allow
role=admin endpoints to app=jump-infra on 22 and 3389 from
ssh, mstsc.exe, Microsoft Remote Desktop; deny everything else from
type=endpoint
- IP list of sanctioned vendor ranges; allow agent processes to that list
only
Order in every recommendation list: allow rules, then denies, then the
enforcement flip.
Verification task (last, every run)
Known PCE and MCP quirks
- Explorer caps at 500 flows per query; the server pins
max_results to
500 and truncated only reflects byte trimming. Check total_pce_flows.
Slice.
include_sources and include_destinations take key=value strings only;
hrefs and IPs are rejected. Multiple entries are ANDed.
get-traffic-flows results over roughly 25k tokens are written to a file by
the harness; read them with the script, not by eye.
- Process names carry the Windows user in the path (
C:\Users\<user>\...);
macOS paths carry no user.
compare-draft-active may report pending changes on non-ruleset objects
while showing zero ruleset changes; report only what the filtered call
returns.
discover-process-egress marks unmanaged RFC1918 hosts as egress; treat
those separately from vendor egress.
- Selective enforcement: "allowed" usually means "no deny matched", not "an
allow rule exists".
Common mistakes
| Mistake |
Fix |
One big get-traffic-flows query for the whole population |
It silently caps at 500 rows. Slice by source label x destination label. |
Passing hrefs or IPs to include_sources |
Label shorthand only. Resolve IPs after the query against the workload map. |
| Two label values in one list to mean OR |
That is AND. Run one query per value. |
| Scoring VS Code, Chrome or python as agents |
Ambiguous hosts go in their own table, unscored, and the scope section says so. |
Counting process - rows as agent traffic |
They are a VEN visibility gap. Report the percentage under coverage. |
| Showing every label on every host |
Defining labels only, same set in every table. |
| Recommending a deny before the allow exists |
Allow rules first, denies second, enforcement last. |
| Writing the report before the verification task |
The six checks run before anything is published. |
Worked example (regression)
Org 5636114, 17 Sep 2026, window 14 to 17 Sep: 206 workloads, 16 endpoints,
15 with agents (8 VDI, 7 laptop), 72 agent flows to 18 internal hosts across
10 apps, 1,618,222 connections, all allowed, all workloads in selective
enforcement, 3 endpoints reaching SWIFT hosts, 5 reaching pay-web01, ruleset
16 port-only, egress 187k connections Claude to Anthropic and 200k ChatGPT to
Cloudflare-fronted IPs, unmanaged host 192.168.2.18 reached by three agents
and scanning endpoints. Overall rating High. A correct run against the same
PCE reproduces these numbers.
1---2name: illumio-agentic-ai-risk-assessment3description: Use when the user asks for an agentic AI risk assessment, AI agent risk, shadow AI assessment, or which endpoints run Claude, ChatGPT, Cursor, Copilot, Codex, Gemini, Windsurf or similar agents on an Illumio PCE. Also use for: what are the AI agents talking to, AI agent reach, an AI agent Sankey or flow diagram of AI processes to internal systems, or assess agentic AI in the org, the PCE or this tenant. Not for general PCE health, ransomware or segmentation assessments (those are illumio-pce-assessment and illumio-daily-assessment). Requires the illumio-mcp MCP server.4---56# Illumio Agentic AI Risk Assessment78Answer four questions a CISO or auditor asks about agentic AI, with evidence9from the PCE:10111. Which managed workloads run agentic AI clients (Claude, ChatGPT, Cursor,12 Copilot, Codex and the like)?132. What internal systems do those agents connect to, on which ports, under14 which user?153. What do the agents send outbound, and to whom?164. Does current policy stop any of it, and what should change?1718Output: a written risk assessment (risk register, regulatory mapping,19prioritised Illumio recommendations) and an interactive Sankey of endpoint to20agent to internal application.2122The skill is descriptive and advisory. It **never provisions policy**. It may23draft rulesets and hand them to the user for review.2425## Files2627| File | Use it for |28| --- | --- |29| `references/agent-catalog.md` | process names, matching rules, vendor egress hints |30| `references/risk-rubric.md` | likelihood and impact scales, R1 to R11, regulatory table |31| `references/report-template.md` | ten-section skeleton with the sentence each section opens with |32| `references/sankey-template.html` | the reach diagram; replace `RAW`, `HOSTS`, `APPS` |33| `scripts/aggregate_flows.py` | merges saved query results into one flow table, emits the Sankey constants |3435Read the catalog and rubric before Step 3. Read the template before Step 7.3637## Preconditions and the one up-front question3839- `illumio-mcp:check-pce-connection` must succeed. If it fails, stop and say40 so.41- Ask **one** question before starting, covering both: report format (Claude42 Doc, .docx, markdown in chat) and lookback window (default: everything the43 PCE holds, typically 4 to 30 days). Ask nothing else up front. Make44 assumptions and state them at the top of the report.45- Optional scope override: a label expression for the source population.46 Default `type=endpoint`. When `type` is not a label dimension, fall back in47 this order: `app=vdi` plus `app=laptop` (two values, so two queries per48 slice and `--population "app=vdi|app=laptop"` for the script), then49 `env=Users`. When several of these exist, `type` wins. If no endpoint-like50 label exists, use all workloads and say so in the scope section.5152## Workflow5354Create a task list with one item per step plus the verification task. Save55every raw tool result to a scratch file; the harness writes large results to56disk anyway, and the script reads them from there.5758### Step 1: Connection and label schema5960```61illumio-mcp:check-pce-connection62illumio-mcp:get-labels max_results: 50063```6465`get-labels` returns a Python-repr list (`Labels: [{'href':..., 'key':...,66'value':...}, ...]`), not JSON. Build a map of label key to values. Decide two things and write them down:6768- the **source-population key** (`type`, else `app`, else `env`)69- the **defining label set** (see Label handling below)7071### Step 2: Workload inventory with labels7273```74illumio-mcp:get-workloads detail_level: "compact", max_results: 1000075```7677Save the result as `scratch/workloads.json`. `compact` returns split-format78JSON (`columns` + `data`) with `href`, `hostname`, `ip_addresses`, `os_type`,79`enforcement_mode` and one column per label key. It is a superset of80`labels_only` and also gives the IPs needed to resolve IP-only destinations81and the per-workload enforcement mode that Step 5 needs (the enforcement82status tool only returns aggregates). Build83`hosts[hostname] = {href, ips, enforcement_mode, labels{key: value}}` for84**every** workload; destinations are resolved against it later. Select the source population by85the chosen label. Record the count and the split by the second-level grouping86(VDI versus laptop from `app`, or whatever the org uses).8788### Step 3: Traffic from the source population, grouped by process8990```91illumio-mcp:get-traffic-flows92 group_by: ["process", "source", "destination", "port", "proto", "policy"]93 include_sources: ["<population label>", "<slice label>"]94 include_destinations: ["<destination slice label>"]95 start_date: "YYYY-MM-DD" end_date: "YYYY-MM-DD"96```9798The result is split-format JSON: `columns` plus `data` rows, with99`process_name` (full path as the VEN reported it), `src_hostname`, `src_ip`,100`dst_hostname`, `dst_ip`, `dst_fqdn`, `port`, `proto`, `policy_decision` and101`num_connections`, sorted by connections descending. `total_pce_flows` is the102number of flows the PCE returned before grouping.103104Two constraints decide the query plan:1051061. **The PCE returns at most 500 flows per query, silently.** The server107 overrides `max_results` to 500, and `truncated` only reports byte-size108 trimming of the response, never the PCE cap. A result with109 `total_pce_flows` of 500 (or 500 rows) is a truncated result. Split until110 every slice comes back under 500.1112. **`include_sources` and `include_destinations` take label shorthand only**112 (`key=value`). Workload hrefs and bare IPs fail with a113 `TrafficQueryFilter` type error. Entries in one list are **ANDed**:114 `["bu=hr", "bu=health"]` returns nothing unless a workload carries both.115116Slicing plan:117118- Outer loop: values of a source-side label that partitions the population119 (`role` works: contractor, admin, employee; `os` as a fallback). When the120 population itself is two label values (`app=vdi`, `app=laptop`), those two121 values are the outermost loop and `role` nests inside.122- OR is always a second query. For `bu=hr` or `bu=health`, run one query123 with `["bu=hr"]` and one with `["bu=health"]`; the script merges them.124- Inner loop: values of a destination-side label that partitions internal125 systems (`bu`; `app` if there is no `bu`).126- One extra query per source slice with **no** destination filter, to catch127 egress and unlabeled destinations.128- Save each result as `flows_<src>_<dst>.json` in the scratchpad. Note the129 row count of every slice; any slice at 500 must be split again (add a third130 label, or split by `proto`, or halve the date window).131132Merge:133134```bash135python3 scripts/aggregate_flows.py \136 --flows scratch/flows_*.json \137 --workloads scratch/workloads.json \138 --egress scratch/egress.json \139 --population "type=endpoint" \140 --out scratch/merged141```142143The script de-duplicates on (process, source, destination, port, proto), tags144each row with the catalog family, extracts the Windows user from the process145path, resolves destinations against the workload map, and splits rows into146**internal** (destination is a managed workload), **egress** (IP or FQDN not147in the map), **ambiguous** (host process) and **unattributed** (no process).148It also prints every input file that held exactly 500 rows. Run `--help` for149options. Outputs: `merged/flows.csv`, `merged/summary.json`,150`merged/sankey_data.js` holding the `RAW`, `HOSTS` and `APPS` constants, and151`merged/sankey.html`, the template with those constants spliced in.152153### Step 4: Egress detail154155```156illumio-mcp:discover-process-egress157 include_sources: ["<population label>"]158 process: ["claude", "chatgpt", "cursor", "codex", "copilot", "gemini", "windsurf", "ollama"]159 lookback_days: 30160 limit: 100161 max_results: 500162```163164- Map destinations to providers with the hints table in the catalog. Confirm165 with the FQDN the PCE resolved or reverse DNS; otherwise write "likely".166- Save the result as `scratch/egress.json` and pass it to the script with167 `--egress`. Each finding carries `process` (basename only), `destination`168 (FQDN if resolved, else IP), `port`, `proto`, `policy_decision`,169 `permitted_today`, `connections`, and `likely_provider` with170 `provider_confidence` when the server could attribute it. Record171 `permitted_today` and connection counts per destination.172- Any RFC1918 destination not in the workload map is an **unmanaged internal173 host**. Report it separately from vendor egress. Cross-check whether the174 same address appears as a *source* of blocked or potentially blocked175 traffic against the endpoints; if so, call it a possible foothold (R7).176177### Step 5: Policy posture178179```180illumio-mcp:get-workload-enforcement-status181illumio-mcp:get-rulesets max_results: 200182illumio-mcp:compare-draft-active resource_type: "rule_sets"183illumio-mcp:enforcement-readiness (optional, for the two or three most-reached apps)184```185186- Enforcement mode counts overall (from the enforcement status tool) and187 for the source population (from the `enforcement_mode` column of the188 Step 2 inventory; the status tool only returns per-app aggregates).189- For each ruleset decide whether it touches the population as consumer or190 any agent-reached app as provider. Note rules whose name or description191 claims process restriction but whose services are port-only (R5). Note192 any/any allows inside app scopes.193- From the draft-versus-active result, say whether the observed state is194 live or partly unprovisioned. Report only what the filtered call returns;195 pending changes on other object types are out of scope.196- In selective enforcement, "allowed" in Explorer means "no deny matched",197 not "an allow rule exists". Say so in the posture section.198199### Step 6: Analysis200201Compute from `merged/flows.csv` and `summary.json`:202203- endpoints with agents, by kind and role; agents per endpoint; endpoints204 per agent205- connections and distinct flows per (agent, app) and per (agent, host, port)206- **regulated reach**: destination carries `compliance=*`, `env=PCI`, or the207 org's equivalent208- **administrative reach**: destination `role` in {dc, jumpbox, bastion,209 admin} or `app` in {ad, jump-infra}, and port in {22, 3389, 88, 389, 445,210 5985, 5986}211- **odd ports**: anything on a regulated or admin host that is not the app's212 expected service (TFTP 69, POP3 110, IMAP 143, 5938 and similar are red213 flags)214- **personal-data reach**: apps named hr, hrm, crm, payroll, people, or215 carrying a `data=pii` style label216- **label contradictions**: if a `risk` or `tier` label exists, compare it to217 observed reach (R10)218219### Step 7: Score and write220221Fill the risk register and regulatory table from `references/risk-rubric.md`222and write the report along `references/report-template.md`. Publish223`merged/sankey.html` as the Sankey artifact (favicon, title "Agentic AI224Reach"). Then run the verification task below before publishing anything.225226## Label handling227228Every workload in the report, source or destination, is shown with its229**defining labels**: the ones that say what the thing is, not all labels.230231- **Always show when present**: `app`, `env`, `role`, `loc`.232- **Show when they add meaning**: `bu`, `type`, `os`, `compliance`, `data` or233 `classification`.234- **Drop**: labels that only steer policy or incident workflow.235 `quarantine.illumio.com=*`, `DFIRBubble=*`, IR bubble labels, `risk=*` when236 it is a policy knob, `kc=*` cluster tags, temporary migration tags.237238Rule of thumb: if the label would change during an incident or a policy239rollout without the asset changing, it is a policy label and is dropped.240Dropped means dropped from the printed label set only. `risk` and `tier` are241still read for the R10 comparison below.242243Decide the set once in Step 1, state it in the scope section, and use the244same set in every table. In the Sankey the destination column shows `app`245with `bu` and `compliance` as small print; the source column shows hostname246with `role` and user.247248If a `risk` label exists, compare it to the evidence and report249contradictions. VDIs labelled Low Risk that carry the most agent reach is a250finding, not decoration.251252## Outputs253254**Report**: the ten sections of `references/report-template.md`, in order,255each opening with its point. Format follows the up-front answer: Claude Doc256by default in Cowork, a markdown file in Claude Code, `.docx` on request via257the docx skill.258259**Sankey**: three columns (endpoint grouped VDI then laptop, agent process,260internal application), link width is connections, colour by agent in the261fixed order Cursor, ChatGPT, Claude, then others. Filters for agent, source262kind and regulated only. Tooltip lists the hosts and ports behind a ribbon.263Sortable flow table below. Light and dark theme. d3 7.9.0 and d3-sankey2640.12.3 from cdnjs. The script fills `RAW`, `HOSTS` and `APPS`; leave the265rest of the template alone.266267**Draft rulesets** (only when the user asks "write the rules"): emit ruleset268drafts as JSON or as `create-ruleset` / `create-deny-rule` call plans. Never269provision. Patterns:270271- deny `type=endpoint` to `compliance=SWIFT` on all services272- process-qualified allow (`chrome.exe`, `msedge.exe` on 443) paired with a273 deny on all services, for browser-only apps274- deny agent process services (`Cursor.exe`, `Claude.exe`, `ChatGPT.exe` and275 the macOS paths) to `role=dc` on 88, 389, 445 and to `app=hrm` on all276- allow `role=admin` endpoints to `app=jump-infra` on 22 and 3389 from277 `ssh`, `mstsc.exe`, `Microsoft Remote Desktop`; deny everything else from278 `type=endpoint`279- IP list of sanctioned vendor ranges; allow agent processes to that list280 only281282Order in every recommendation list: allow rules, then denies, then the283enforcement flip.284285## Verification task (last, every run)286287- [ ] Endpoint count in the summary equals distinct sources in the merged288 table289- [ ] Every (agent, app) total in the chart equals the sum of its appendix290 rows291- [ ] Every regulated flag in the inventory has at least one appendix row to292 a compliance-labelled host293- [ ] No query slice returned exactly 500 rows; if one did, it was split294 further or the report says the slice is truncated295- [ ] Every ruleset named in the posture table exists in `get-rulesets`296 output297- [ ] No recommendation references a label value that does not exist in the298 org299300## Known PCE and MCP quirks301302- Explorer caps at 500 flows per query; the server pins `max_results` to303 500 and `truncated` only reflects byte trimming. Check `total_pce_flows`.304 Slice.305- `include_sources` and `include_destinations` take `key=value` strings only;306 hrefs and IPs are rejected. Multiple entries are ANDed.307- `get-traffic-flows` results over roughly 25k tokens are written to a file by308 the harness; read them with the script, not by eye.309- Process names carry the Windows user in the path (`C:\Users\<user>\...`);310 macOS paths carry no user.311- `compare-draft-active` may report pending changes on non-ruleset objects312 while showing zero ruleset changes; report only what the filtered call313 returns.314- `discover-process-egress` marks unmanaged RFC1918 hosts as egress; treat315 those separately from vendor egress.316- Selective enforcement: "allowed" usually means "no deny matched", not "an317 allow rule exists".318319## Common mistakes320321| Mistake | Fix |322| --- | --- |323| One big `get-traffic-flows` query for the whole population | It silently caps at 500 rows. Slice by source label x destination label. |324| Passing hrefs or IPs to `include_sources` | Label shorthand only. Resolve IPs after the query against the workload map. |325| Two label values in one list to mean OR | That is AND. Run one query per value. |326| Scoring VS Code, Chrome or python as agents | Ambiguous hosts go in their own table, unscored, and the scope section says so. |327| Counting process `-` rows as agent traffic | They are a VEN visibility gap. Report the percentage under coverage. |328| Showing every label on every host | Defining labels only, same set in every table. |329| Recommending a deny before the allow exists | Allow rules first, denies second, enforcement last. |330| Writing the report before the verification task | The six checks run before anything is published. |331332## Worked example (regression)333334Org 5636114, 17 Sep 2026, window 14 to 17 Sep: 206 workloads, 16 endpoints,33515 with agents (8 VDI, 7 laptop), 72 agent flows to 18 internal hosts across33610 apps, 1,618,222 connections, all allowed, all workloads in selective337enforcement, 3 endpoints reaching SWIFT hosts, 5 reaching pay-web01, ruleset33816 port-only, egress 187k connections Claude to Anthropic and 200k ChatGPT to339Cloudflare-fronted IPs, unmanaged host 192.168.2.18 reached by three agents340and scanning endpoints. Overall rating High. A correct run against the same341PCE reproduces these numbers.