@matterfact/embed 0.11.0 → 0.11.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,518 +44,38 @@ Ships types. In the Next.js App Router it needs a `'use client'` boundary.
44
44
  `type="module"` is required — the loader is an ES module. `src` (where the loader is
45
45
  served) and `data-origin` (where the chat is served) are different things.
46
46
 
47
- ## Where it renders
48
-
49
- By default: a floating launcher in the corner, draggable, with a right-click menu.
50
-
51
- Prefer it in **your own** side panel, drawer or tab? Add `inline` — it renders where you
52
- put it and fills that space, sized by your CSS. No launcher is drawn and it never resizes
53
- itself; showing/hiding your panel is the open/close control.
54
-
55
- ```tsx
56
- <aside className="w-[380px] border-l">
57
- <MatterfactAgent inline publishableKey="pk_live_…" />
58
- </aside>
59
- ```
60
-
61
- Script-tag equivalent: `data-container="#your-panel"`.
62
-
63
47
  ## Embedding an artifact
64
48
 
65
- Drop a matterfact **artifact** (a chart, table, dashboard you built in matterfact) straight
66
- into your page:
67
-
68
49
  ```tsx
69
50
  import { MatterfactArtifact } from '@matterfact/embed/react';
70
51
 
71
- <MatterfactArtifact slug="tsla-liquidity" owner="you@firm.com" token="…" />;
72
- ```
73
-
74
- `slug` and `owner` identify the artifact; `token` is the read-only embed token you mint for
75
- it (the **Share → Embed** dialog in matterfact generates the whole snippet, token included).
76
- It renders chrome-free in a sandboxed iframe and fills its container — size it with
77
- `className`/`style`.
78
-
79
- **The payoff when the agent is on the same page:** embed a `<MatterfactAgent>` too and the
80
- agent automatically sees the artifact you embedded and can answer questions about the data
81
- behind it — no wiring. The agent only ever gets read access to that artifact's own data, and
82
- the token never reaches the model. Nothing extra to configure.
83
-
84
- Script-tag / non-React hosts can use the iframe directly (the Share dialog shows this form):
85
-
86
- ```html
87
- <iframe
88
- src="https://app.matterfact.com/embed/artifacts/tsla-liquidity?owner=you@firm.com&t=…"
89
- width="100%"
90
- height="600"
91
- style="border:0"
92
- ></iframe>
93
- ```
94
-
95
- ## Linking your app's matterfact content
96
-
97
- A co-embedded `<MatterfactArtifact>` (above) is found automatically. But your app may
98
- render matterfact content another way — a report/dossier you pulled from matterfact and
99
- render yourself, or artifacts spread across many routes. Tell the agent where that content
100
- lives with a **site map**: your routes, each tagged with what kind of matterfact content
101
- it carries. The agent uses it to reason about — and navigate toward — content the user
102
- isn't currently looking at.
103
-
104
- ```tsx
105
- <MatterfactAgent
106
- sitemap={[
107
- {
108
- path: '/company/:ticker/dossier',
109
- label: 'Company Dossier',
110
- content: { kind: 'mf-document', doctype: 'company_dossier' },
111
- },
112
- {
113
- path: '/talent/exec-departures',
114
- label: 'Exec Departures',
115
- content: { kind: 'mf-artifact', slug: 'exec-departure-tracker' },
116
- },
117
- { path: '/screener', label: 'Screener', content: { kind: 'host-data' } },
118
- ]}
119
- />
120
- ```
121
-
122
- ```js
123
- // script tag — same shape
124
- window.matterfact = {
125
- sitemap: [
126
- /* … */
127
- ],
128
- };
129
- ```
130
-
131
- Each entry is **classification only** — a route pattern, a label, and a `content` tag:
132
-
133
- - `{ kind: 'mf-artifact', slug }` — a matterfact artifact rendered on that route.
134
- - `{ kind: 'mf-document', doctype }` — a matterfact document/report (dossier, etc.).
135
- - `{ kind: 'host-data' }` — your own data; not matterfact content.
136
-
137
- **Never put a capability token in the site map.** It is app-wide and rides on every turn;
138
- the concrete artifact/document on the _current_ page arrives separately (the live artifact
139
- iframe, or per-page context — see below), where the token belongs.
140
-
141
- ### Documents on the current page
142
-
143
- For a matterfact **document** the user is looking at, hand the agent its id via page
144
- context (the same `context.entities` you already declare — see _What the agent can see_):
145
-
146
- ```tsx
147
- <MatterfactAgent
148
- getPageContext={() => ({
149
- route: '/company/:ticker/dossier',
150
- entities: [
151
- {
152
- kind: 'document',
153
- id: currentDoc.mfDocId, // the matterfact MF_DOC_ID
154
- label: currentDoc.title,
155
- data: { href: location.href }, // optional: enables the panel's "open on page" deeplink
156
- },
157
- ],
158
- })}
159
- />
52
+ <MatterfactArtifact slug="" owner="you@firm.com" token="…" widgetOrigin="https://app.matterfact.com" />;
160
53
  ```
161
54
 
162
- The agent can then cite the document inline as a chip; clicking it opens a read-only viewer
163
- in the widget (authorized by the signed-in user's own matterfact access no token), with an
164
- optional **Open on page** deeplink back to your route. matterfact resolves the document from
165
- its own systems by `MF_DOC_ID`; it never reads your database.
55
+ Renders a matterfact chart/table/dashboard chrome-free in your page. Embed a
56
+ `<MatterfactAgent>` on the same page and it automatically sees the artifact and can answer
57
+ about the data behind it no wiring, and the token never reaches the model.
166
58
 
167
- ### Content the user _isn't_ looking at
59
+ ## Documentation
168
60
 
169
- The site map tells the agent a route _carries_ matterfact content; the two props below let it
170
- actually **reach** that content from anywhere, without the user navigating there first.
61
+ The full integration guide placement, page context, linking your matterfact content,
62
+ host tools, telemetry, authentication (incl. Microsoft Entra), and debugging is available
63
+ to partners after signing in, in the **Embedding** section of the matterfact docs:
171
64
 
172
- **Boards on any route — `artifacts`.** Declare your matterfact artifacts once and the agent
173
- can open any of them from any page, even routes where you don't embed the `<MatterfactArtifact>`
174
- iframe:
65
+ **[app.matterfact.com/docs](https://app.matterfact.com/docs)** *(partner sign-in required)*
175
66
 
176
- ```tsx
177
- <MatterfactAgent
178
- artifacts={[
179
- { slug: 'exec-departure-tracker', owner: 'you@firm.com', token: '…', label: 'Talent Bank' },
180
- { slug: 'global-datacenter-buildout', owner: 'you@firm.com', token: '…', label: 'Data Centers' },
181
- ]}
182
- />
183
- ```
184
-
185
- ```js
186
- // script tag — same shape
187
- window.matterfact = { artifacts: [{ slug: '…', owner: '…', token: '…', label: '…' }] };
188
- ```
189
-
190
- This replaces the old workaround of mounting hidden, zero-size `<MatterfactArtifact>` iframes
191
- just to make a board's grant reachable. The `token` stays in this host-page config and in the
192
- grant channel — it never enters the chat transcript or reaches the model.
193
-
194
- **A resolver for content you look up per-request — `resolve`.** When your set of documents or
195
- boards is per-user or per-ticker (too large to declare), give the agent a function that turns a
196
- classification into a concrete reference. matterfact defines the tool contract (name, arguments,
197
- result shape, and how the agent uses it — so it costs you no agent-instruction budget); you
198
- supply the implementation:
199
-
200
- ```tsx
201
- <MatterfactAgent
202
- resolve={{
203
- // doctype + a key (e.g. a ticker) -> the matterfact document id, from YOUR lookup.
204
- document: async ({ doctype, key }) => {
205
- const row = await myHub.lookup(doctype, key);
206
- return { id: row.MF_DOC_ID, label: `${key} ${doctype}`, href: hrefFor(doctype, key) };
207
- },
208
- // a slug -> a read grant, when you resolve boards dynamically instead of declaring them.
209
- artifact: async ({ slug }) => myHub.artifactGrant(slug),
210
- }}
211
- />
212
- ```
213
-
214
- The agent calls `mf.resolveDocument` / `mf.resolveArtifact` when the site map shows the content
215
- lives on another route, gets back an id (and, for a document, an optional `href` deeplink), and
216
- cites it as a chip — exactly as for on-page content. **matterfact never queries your data**: your
217
- function runs the lookup, matterfact only ever receives the id it returns. `resolve` is a host
218
- tool (see [Host tools](#host-tools)), so your app's `host_tools` policy must allow `mf.*` — it
219
- does by default.
220
-
221
- ## Signing users in
222
-
223
- By default the widget runs its own sign-in in a popup (the only way an embedded frame can
224
- get a real first-party session). If your app already holds an identity this matterfact
225
- deployment trusts, hand the token over for silent sign-in:
226
-
227
- ```tsx
228
- <MatterfactAgent getAuthToken={() => auth.currentUser?.getIdToken() ?? null} />
229
- ```
230
-
231
- ```js
232
- // script tag
233
- window.matterfact = { getEmbedAuthToken: () => getToken() };
234
- ```
235
-
236
- We verify the token either way — this skips a redundant login, it doesn't bypass auth.
237
-
238
- ### Microsoft Entra (single-tenant)
239
-
240
- On a single-tenant Entra deployment there are **two app registrations**, and mixing them up
241
- is the thing that catches everyone:
242
-
243
- - the **matterfact** app registration — you created it in your tenant and gave us its
244
- client id + secret. It _exposes_ the scope.
245
- - the app you're **embedding into** (e.g. your portal) — a **different** registration with
246
- its **own** client id. It's the one your users sign into.
247
-
248
- The token you pass to `getAuthToken` must be an **access token** for the matterfact app's
249
- scope — **not** your app's ID token. An ID token is addressed to your app, not matterfact's,
250
- and is rejected.
251
-
252
- A one-time setup on the matterfact registration (expose the scope, accept v2 tokens,
253
- pre-authorise your embedding app) is run by your Entra admin — the script and full
254
- walkthrough are in the [integration guide](https://app.matterfact.com/docs/api). Once it's
255
- done, your embedding app requests that scope with MSAL and hands the access token over:
256
-
257
- ```tsx
258
- import { useMsal } from '@azure/msal-react';
259
-
260
- const { instance, accounts } = useMsal(); // configured with YOUR app's client id
261
-
262
- const getAuthToken = async () => {
263
- const { accessToken } = await instance.acquireTokenSilent({
264
- scopes: ['api://<matterfact-client-id>/embed.access'],
265
- account: accounts[0],
266
- });
267
- return accessToken; // access token, not the ID token
268
- };
269
-
270
- <MatterfactAgent publishableKey="pk_live_…" getAuthToken={getAuthToken} />;
271
- ```
272
-
273
- The access token must carry an `email` / `preferred_username` / `upn` claim — the identity
274
- is keyed on it. Until the scope is set up, the default sign-in popup works with no
275
- configuration.
276
-
277
- ## What the agent can see
278
-
279
- Nothing until a user opens the chat; then a semantic snapshot of the **visible** page.
280
- Never captured, with no setting to enable it: keystrokes, input/textarea values, passwords,
281
- cookies, storage, the raw DOM, screenshots, network traffic, or anything inside
282
- `data-mf-private`. PII is scrubbed in the browser before anything leaves the page. The
283
- exact per-turn payload is inspectable in the widget's "what can it see?" panel.
284
-
285
- Declare what your page is, and it rides on every turn:
286
-
287
- ```js
288
- window.matterfact = {
289
- context: {
290
- route: '/orders/:id',
291
- description: 'Acme order 8813 — 3 items, unpaid',
292
- entities: [{ kind: 'x:order', id: '8813', label: 'Acme order' }],
293
- },
294
- };
295
- ```
296
-
297
- ### Keeping context fresh (virtualized SPAs)
298
-
299
- The static `context` above is read on load and on URL navigation. In a **virtualized SPA**,
300
- what the agent should know changes without the path changing — a team switch, a filter edit,
301
- a row selection — and a static object goes stale until the next navigation. Give a **pull
302
- callback** instead and the widget reads it **fresh at the start of every turn** — symmetric
303
- with `getAuthToken`:
304
-
305
- ```tsx
306
- <MatterfactAgent
307
- publishableKey="pk_live_…"
308
- getPageContext={() => ({
309
- route: '/portfolios/:id',
310
- description: `Growth Sleeve${selected ? ` — focused on ${selected}` : ''}`,
311
- entities: holdings.map((h) => ({
312
- kind: 'x:ticker',
313
- id: h.ticker,
314
- label: h.ticker,
315
- })),
316
- data: { selected },
317
- })}
318
- />
319
- ```
320
-
321
- ```js
322
- // script tag — same idea, a global callback (supersedes window.matterfact.context)
323
- window.matterfact = {
324
- getPageContext: () => ({ route: '/portfolios/:id', description: '…' }),
325
- };
326
- ```
327
-
328
- May be async (read a store, await a selector). Auto-detected fields (url/path/title, embedded
329
- artifacts) are still merged on top. Resolution order, first non-null wins: the React
330
- `getPageContext` prop → `window.matterfact.getPageContext()` → the static `window.matterfact.context`.
331
- See [`examples/embed-demo`](examples/embed-demo) (React) and [`examples/host-panel.html`](examples/host-panel.html) (script tag).
332
-
333
- Want none of this? `pageContext={false}` (or `data-page-context="off"` on the script
334
- tag) turns off page observation entirely — not just the declared context object above,
335
- but the snapshot, the focus/activity stream, and artifact grants too. The loader never
336
- installs the observers that would collect any of it, and a request to send a snapshot
337
- gets nothing back. Default is on: seeing the page is the point of the widget.
338
-
339
- ```tsx
340
- <MatterfactAgent publishableKey="pk_live_…" pageContext={false} />
341
- ```
342
-
343
- ## Hoist apps
344
-
345
- If the host page is an [XH Hoist](https://xh.io/) SPA — `window.XH` is present — the widget
346
- automatically extracts rich page context and a snapshot from it. No `window.matterfact.context`,
347
- no `getPageContext`, no host code at all: the grids, charts, and navigation already mounted in
348
- the page are the source of truth. On any other page this is skipped entirely and the widget falls
349
- back to the generic DOM snapshot described above.
350
-
351
- This affects two different messages the widget sends, independently:
352
-
353
- - The per-turn **context** (route + one-line description) still prefers an explicit
354
- `getPageContext` / `window.matterfact.context` when the host declares one — Hoist is only the
355
- fallback when neither is set.
356
- - The on-demand **snapshot** (what the agent sees when it looks at the page) is Hoist-first,
357
- unconditionally, ahead of the generic DOM walk — whether or not the host also declares context.
358
-
359
- What gets extracted:
360
-
361
- - **Route + description** — `XH.routerState`, turned into a one-line summary, e.g. `"Viewing
362
- default.company.tsr, 1 selected, 1 grid(s)."`
363
- - **Grids** — each mounted `GridModel` that has rows or a selection (an empty, not-yet-loaded
364
- grid — e.g. an off-view tab — is skipped), as a table: columns (a grouped column is flattened
365
- to its visible leaves and prefixed with its group, e.g. `"5Y Value"`), the current selection,
366
- and up to `rows` data rows with a total count.
367
- - **Charts** — each mounted `ChartModel` with at least one series, summarised per series (point
368
- count, min, max, last) — never the raw series.
369
- - **Site map** — each mounted `TabContainerModel`, joined to the router's routes: the app's
370
- navigable destinations, grouped by nav level (top tabs, a per-record sidebar, …), with the
371
- current one marked. This leads the snapshot, since it barely changes turn to turn.
372
-
373
- It reads roughly like the ARIA snapshot above, with its own block types:
374
-
375
- ```
376
- - navigation "Site":
377
- - link "Companies" → /app/companies
378
- - link "Company Analysis" [current] → /app/company
379
- - table "Peer TSR":
380
- - rowgroup "columns":
381
- - columnheader "Ticker"
382
- - columnheader "1Y TSR"
383
- - rowgroup "rows":
384
- - row:
385
- - cell "SITE"
386
- - cell "12.3%"
387
- - text: "showing 1 of 412 rows"
388
- - figure "TSR vs Index":
389
- - text: "series SITE: 4 points, min 1, max 10, last 10"
390
- ```
391
-
392
- ### Configuring it
393
-
394
- ```js
395
- window.matterfact = {
396
- hoist: {
397
- rows: 50, // rows sent per grid before truncating (default 50)
398
- excludeModels: [], // class names ('ChartModel') or specific model ids to omit
399
- actions: { navigate: 'off' }, // see "Letting the agent navigate" below
400
- },
401
- };
402
- ```
403
-
404
- | Key | Type | Default | What |
405
- | ------------------ | ------------------------------ | ------- | -------------------------------------------------------------------------------------------------------------------------------- |
406
- | `rows` | `number` | `50` | Rows sent per grid before truncating. The full row count still rides along. |
407
- | `excludeModels` | `string[]` | `[]` | Model class names (drops every instance) or specific model ids to omit — applies to grids, charts, and nav tab containers alike. |
408
- | `actions.navigate` | `'off' \| 'confirm' \| 'auto'` | `'off'` | Whether the agent can drive navigation — see below. |
409
-
410
- ### Letting the agent navigate
411
-
412
- Set `actions.navigate` and the widget advertises a `hoist.navigate` tool bounded to exactly the
413
- destinations in the CURRENT site map — the agent can't ask for somewhere that isn't reachable
414
- from where the user actually is, and every call is re-validated against the live site map (and
415
- the live policy), never whatever the widget last cached, before it runs:
416
-
417
- - `'off'` (default) — the tool isn't advertised at all.
418
- - `'confirm'` — advertised, and the widget shows an approval card the user must accept before
419
- each call.
420
- - `'auto'` — advertised and runs without asking.
421
-
422
- ```tsx
423
- <MatterfactAgent publishableKey="pk_live_…" actions={{ navigate: 'confirm' }} />
424
- ```
425
-
426
- ```js
427
- // script tag — same idea, the global the React prop above is sugar for
428
- window.matterfact = { hoist: { actions: { navigate: 'confirm' } } };
429
- ```
430
-
431
- Changing `actions` on `<MatterfactAgent>` remounts the widget (a fresh chat session) so the new
432
- policy is advertised immediately — it's identity-defining config, not a live callback like
433
- `getPageContext`.
434
-
435
- Everything else about page observation still applies here: values are redacted before anything
436
- leaves the page, row counts are capped, and `pageContext={false}` turns this off along with
437
- everything else (see above).
438
-
439
- ## Host tools
440
-
441
- `hoist.navigate` above is one instance of a general mechanism: **the host page can offer the
442
- agent tools to call**. There are three kinds, distinguished by who owns the contract and who
443
- runs the code:
444
-
445
- | Prefix | Contract | Runs where | Examples |
446
- | ---------- | ---------- | ------------------- | ------------------------------------------------ |
447
- | `hoist.*` | matterfact | the widget's adapter | `hoist.navigate` (above) |
448
- | `mf.*` | matterfact | **your** function | `mf.resolveDocument`, `mf.resolveArtifact` (above) |
449
- | `app.*` | **you** | **your** function | anything you declare |
450
-
451
- `hoist.*` and `mf.*` you get by turning on the features above. `app.*` is your own tools, which
452
- you declare with the `tools` prop:
453
-
454
- ```tsx
455
- <MatterfactAgent
456
- tools={[
457
- {
458
- name: 'exportBook', // advertised to the agent as `app.exportBook`
459
- description: 'Export the current book to CSV.',
460
- inputSchema: { type: 'object', properties: { scope: { type: 'string' } } },
461
- confirm: 'required', // 'required' (default) shows an approval card; 'auto' runs without asking
462
- handler: async ({ scope }) => myApp.exportBook(scope),
463
- },
464
- ]}
465
- />
466
- ```
467
-
468
- ```js
469
- // script tag — same shape
470
- window.matterfact = { tools: [{ name: '…', description: '…', handler: async () => {} }] };
471
- ```
472
-
473
- Names are namespaced into `app.` automatically, so a host tool can never collide with a
474
- matterfact one. A tool's `handler` result and any error it throws are **redacted before leaving
475
- the page**; a handler that throws, rejects, or hangs (beyond 30s) becomes a normal "not
476
- completed" result the agent reads and adapts to — never a crash. Like `actions`, changing the
477
- _shape_ of your tools (a name, description, schema, or confirm policy) remounts the widget so the
478
- new set is advertised; changing only a handler's body does not, so an inline `handler` closure
479
- won't churn the chat session.
480
-
481
- ### Who decides which tools are exposed
482
-
483
- Advertising a tool is necessary but not sufficient: **which tools an app may actually expose is
484
- a server-side setting** on your embed app (`host_tools`: `off` / `allowlist` / `all`, plus an
485
- allow-list of names). The default allows matterfact's own `hoist.*` and `mf.*` and denies
486
- arbitrary `app.*` until an operator grants it — so `resolve` works out of the box, and turning on
487
- your first `app.*` tool is a deliberate decision made in the admin console, not something a page
488
- can do on its own. Ask your matterfact contact to enable the `app.*` names you need.
489
-
490
- ### Telemetry — `onEvent`
491
-
492
- One hook pipes the whole widget into your own observability — lifecycle (`ready`, `open`,
493
- `close`, `navigate`, `auth`, `error`), each chat turn (`chat`), and every tool call (`tool`):
494
-
495
- ```tsx
496
- <MatterfactAgent
497
- onEvent={(e) => myTelemetry.track(`mf.${e.type}`, e)}
498
- />
499
- ```
500
-
501
- Fire-and-forget and fully isolated — a throw or a slow callback here never blocks or breaks the
502
- widget, a tool call, or a run. Chat events are content-free (phase + opaque chat id, never
503
- message text). `onToolEvent` still works as a tool-only subset for back-compat.
504
-
505
- **→ Full event reference: [`docs/telemetry.md`](docs/telemetry.md).**
506
-
507
- ## Microphone / dictation
508
-
509
- The composer supports voice dictation; the loader grants the iframe `allow="microphone"`.
510
- If your site sends a `Permissions-Policy` response header that denies `microphone`, the
511
- browser blocks the widget's mic regardless — add
512
- `microphone=(self "https://app.matterfact.com")` (or your widget origin) to that header.
513
-
514
- ## Debugging a live embed
515
-
516
- Add `?mfdev=1` to your page's URL to open a panel showing exactly what the widget sends —
517
- on a real deployment, no dev build. It grants nothing.
518
-
519
- Building the host page yourself and don't want to depend on a query param? `dev={true}`
520
- does the same thing programmatically — either trigger works, and they're OR'd together.
521
- There's no script-tag attribute for it on purpose: the URL trigger exists precisely so an
522
- engineer can flip it on a live deployment without redeploying the customer's site; the
523
- prop is for a host that wants it wired into their own debug tooling instead.
524
-
525
- ```tsx
526
- <MatterfactAgent
527
- publishableKey="pk_live_…"
528
- dev={process.env.NODE_ENV !== 'production'}
529
- />
530
- ```
531
-
532
- ## Security model
533
-
534
- - The publishable key is public and useless alone; the boundary is the user holding a
535
- matterfact identity obtained through sign-in.
536
- - Origin pinning is a misconfiguration guard, not a security boundary.
537
- - The page snapshot is untrusted reference data to the agent — grounding, never
538
- instructions.
67
+ Don't have access yet? Ask your matterfact contact.
539
68
 
540
69
  ## Examples
541
70
 
542
- Runnable examples ship inside this package (under `examples/`) and are browsable without
543
- cloning anything:
544
-
545
- - **`examples/embed-demo/`** — a tiny Vite + React app that mounts `<MatterfactAgent inline>`
546
- in a side panel next to an embedded `<MatterfactArtifact>`, configured entirely via `.env`. Also
547
- ships an opt-in mock Hoist app (`?hoist=1`) to try the automatic Hoist-app path above with no
548
- real Hoist deployment — see its own README.
549
- Browse it on [unpkg](https://unpkg.com/browse/@matterfact/embed/examples/embed-demo/), or
550
- after `npm install @matterfact/embed` find it at
551
- `node_modules/@matterfact/embed/examples/embed-demo/` — copy it out, `npm install`, `npm run dev`.
552
- - **`examples/host-panel.html`** — the inline-placement pattern as a single HTML file (script-tag loader).
553
- - **`examples/demo-host/`** — a zero-dependency host site proving the script-tag loader end to end.
554
-
555
- ## Full guide
71
+ Runnable examples ship inside this package (under `examples/`):
556
72
 
557
- The complete integration reference endpoints, single-tenant Entra setup, troubleshooting
558
- is served to partners at [app.matterfact.com/docs/api](https://app.matterfact.com/docs/api).
73
+ - **`examples/embed-demo/`** a Vite + React app mounting an inline agent next to an
74
+ embedded artifact, with host tools and a live `onEvent` activity log. Find it at
75
+ `node_modules/@matterfact/embed/examples/embed-demo/` after installing — copy it out,
76
+ `npm install`, `npm run dev`.
77
+ - **`examples/host-panel.html`** — the inline pattern as a single HTML file (script-tag loader).
78
+ - **`examples/demo-host/`** — a zero-dependency site proving the script-tag loader end to end.
559
79
 
560
80
  ## License
561
81
 
package/dist/react.cjs CHANGED
@@ -2468,7 +2468,7 @@ function MatterfactArtifact({
2468
2468
  title: `matterfact artifact ${slug}`,
2469
2469
  className: className2,
2470
2470
  style: { width: "100%", height: 600, border: 0, ...style },
2471
- sandbox: "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox"
2471
+ sandbox: "allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox allow-downloads"
2472
2472
  }
2473
2473
  );
2474
2474
  }