Security and Trust Between Site and Agent|INEMA.CLUB-PRO

Lesson 4 · 18 min

Who sees the tool: same origin, exposedTo, iframes and Permissions-Policy

By default, only the page itself and same-origin pages can see the tool. An iframe from another origin shows up to the top page only with three consents, and a header turns everything off. In this lesson you measure the seven cases in Chromium and put Permissions-Policy: tools=() on the pages that do not need WebMCP.

Start the lesson
Two overlapping browser windows on a dark monitor, one inside the other, with a connector between them.

In this lesson you will

  1. 1Origin = scheme + host + port, and the spec's exposure default
  2. 2The iframe's three consents: allow="tools", exposedTo and fromOrigins
  3. 3Permissions-Policy: tools=() turns the API off before any script runs
  4. 4Seven cases measured in Chromium 156 against the spec of October 16, 2026

Goal: measure who sees each tool (same origin, iframe, exposedTo, fromOrigins) and turn WebMCP off with Permissions-Policy where it should not exist.

01The four exposure controls

Click each one. Top = the main page; iframe = a page from another origin inside it.

Click each block in the diagram to see what it does.

Measured in Chromium 156.0.8078.4 with the WebMCP test flag (experimental as of October 19, 2026). The spec's origin boundaries section (§6.3.4) is still just a to-do note.

02Seven cases, two local origins

The top page runs on 127.0.0.1 on one port, and the iframe runs on 127.0.0.1 on another port. Same machine, different port: different origin.

Terminal · exposure
cd ~/projetos/formacao-webmcp-seguranca-v72/kit
node scripts/exposicao.mjs; echo "saída: $?"
File · kit/exposicao/filho.html
<!doctype html>
<!-- filho.html — the page inside the iframe (it can be from another origin). Tries to register a tool.
     Parameter (lab only): ?exposto=<origin>  becomes  registerTool(..., { exposedTo: [<origin>] }) -->
<meta charset="utf-8">
<title>iframe</title>
<script>
  (async () => {
    const q = new URLSearchParams(location.search);
    const mc = document.modelContext;
    let estado = 'sem WebMCP';
    if (mc) {
      try {
        const opcoes = q.get('exposto') ? { exposedTo: [q.get('exposto')] } : {};
        await mc.registerTool({ name: 'ferramenta_iframe', description: 'Ferramenta do iframe.',
          inputSchema: { type: 'object', properties: {} }, execute: async () => ({ ok: true }) }, opcoes);
        estado = 'registrou';
      } catch (e) { estado = 'recusou: ' + e.name; }
    }
    parent.postMessage({ iframe: estado }, '*');
  })();
</script>
Example from the repository
== Chromium 156.0.8078.4 (Playwright) · modo: nativo
confere 0 · iframe da MESMA origem, sem nada → vê: mesma origem é o padrão
confere A · iframe de outra origem, sem allow="tools" → iframe recusado (NotAllowedError)
confere B · allow="tools", sem exposedTo → topo não vê
confere C · allow + exposedTo:[topo], topo sem fromOrigins → topo não vê
confere D · allow + exposedTo:[topo] + fromOrigins:[iframe] → topo VÊ a ferramenta do iframe
confere E · topo com Permissions-Policy: tools=() → topo e iframe recusados
confere F · exposedTo com origem http:// pública → iframe recusado (SecurityError)
7/7 casos conferem com a spec
saída: 0

Only in case D does the top page see the iframe's tool, and the result carries its origin (ferramenta_iframe@http://127.0.0.1:…). The agent can use that field to avoid mixing up whose tool it is.

03Rules for your site

1. The default is enough

If the tool belongs only to your page, don't use exposedTo. Same origin already sees it, and no other origin needs to.

04The header in practice

The kit's server sends tools=() on the privacy page and nothing on the main page. Alongside it, the same header on three common servers.

Terminal · header
cd ~/projetos/formacao-webmcp-seguranca-v72/kit
node scripts/servir.mjs & P=$!
until curl -s -o /dev/null http://127.0.0.1:8742/; do sleep 0.3; done
curl -sI http://127.0.0.1:8742/privacidade.html | grep -i '^permissions-policy'
curl -sI http://127.0.0.1:8742/ | grep -ci '^permissions-policy'
kill $P; echo "saída: $?"
Template · nginx
location = /privacidade.html {
    add_header Permissions-Policy "tools=()" always;
}
Template · Express
// before express.static
app.use('/privacidade.html', (req, res, next) => {
  res.set('Permissions-Policy', 'tools=()');
  next();
});
Template · _headers (Netlify, Cloudflare Pages)
/privacidade.html
  Permissions-Policy: tools=()
Example from the repository
página em http://127.0.0.1:8742/
Permissions-Policy: tools=()
0
saída: 0

The privacy page sends tools=(); the main page sends 0 Permissions-Policy headers (it needs the API).

05Practice

Measure and turn it off.

E10 · Seven cases

5 min

Run the exposure block.

Done when: 7/7 casos conferem com a spec and saída: 0.

E11 · Kit header

3 min

Run the header block.

Done when: Permissions-Policy: tools=() on the privacy page and 0 on the main page.

E12 · Your site

20 min

Pick the pages on your site that don't register a tool (login, payment, privacy), set the header on your server, and check each one with curl -sI [page address] | grep -i permissions-policy.

Done when: Each page on the list responds with permissions-policy: tools=().

06Lesson quiz

Question 1 of 4

An iframe from another origin registered a tool with exposedTo pointing at the top page, but the top page can't see it. What's missing?skill

07In three sentences

DefaultSame origin sees it; another origin sees nothing.
IframeOnly with allow="tools", exposedTo and fromOrigins together.
Turn offtools=() on pages that don't need the API.

Going further

Previous lessonNext lesson