skills

26 skillsgithubanonymous
all skillsrag-media-view

rag-media-view

Use when asked to show, view, render, or display an image or video that lives in a MultimodalRAG dataset (a PCAI deployment), when a rag-mcp-server /api/datasets/.../files/... URL appears in markdown and fails to render, or when a tool result must become a viewable local file. Covers the SSO-gate root cause, the fetch-to-disk-and-render path, and the MCP_FILE_BYTES_MAX ceiling. Do NOT use for uploading media into a dataset or for dataset search itself.

raw markdown

Body

# Viewing RAG dataset media

## The one thing to understand first

**A RAG media URL will almost never render as markdown in a browser on a
different origin.** Not a harness bug, not a config mistake — a browser rule.

Two independent gates stack up:

1. **The app host sits behind oauth2-proxy.** A `CUSTOM` AuthorizationPolicy on
   the istio-ingressgateway 302s *every* request on that host to Keycloak before
   FastAPI runs. That includes the HMAC-signed `?token=` URLs the MCP server
   mints precisely so `<img>` tags can fetch protected media. The app-level token
   is never evaluated.
2. **The gateway cookie is `SameSite=Lax`.** Lax cookies are sent on top-level
   navigation but NOT on cross-site subresource loads. So clicking the link works
   (you have a session); embedding it in a page on another origin does not (the
   `<img>` fires cookieless and is 302d again).
3. **The agent sandbox cannot reach that host AT ALL** (2026-10-29, measured).
   Outbound HTTP from `bash` goes through a stub proxy at `127.0.0.1:3128` that
   answers every request with a 723-byte HTML 302, and DNS for the media host
   does not resolve. `curl` and `web_fetch` are dead ends — one turn burned ~4
   steps rediscovering this. Do NOT probe URL reachability, and do NOT
   hand-assemble a signed URL from a guessed token (that produced an invented
   token AND an expired one): go straight to the MCP tool
   `get_dataset_file_bytes`, which authenticates server-side.

Measured, on a controlled host pair:

| Context | Cookie sent? | Renders? |
|---|---|---|
| Same-site `<img>` (app page -> app host) | yes | yes |
| Cross-site `<img>` (foreign page -> app host) | **no** | **no** |
| Top-level navigation (clicking the link) | yes | yes |

A same-site front end (e.g. Open WebUI on the same registrable domain) works for
exactly this reason — the cookie rides along. A local harness on `127.0.0.1` is a
foreign site, so it never does.

**Do not try to fix this client-side.** No CSP tweak, config flag, or harness
setting changes it. The browser will not attach a Lax cookie to a cross-site
subresource. Route around it.

## The working path: fetch bytes, write to disk, render the local file

**HOW TO CALL THE TOOLS (2026-10-29 — this changed, and the old instructions
here were wrong).** MCP tools arrive through the gateway's server-side RELAY,
not through `tools[...]` bindings inside `run_code`. Concretely:

1. `pcai_tools` is a DIRECT tool call (a normal function call, NOT inside
   `run_code`). Call it with `{action:"describe", server:"mm-rag"}`.
2. That PROMOTES mm-rag: its tools (`mm-rag_list_datasets`,
   `mm-rag_search_dataset`, `mm-rag_get_dataset_file_bytes`, …) become
   directly callable for the rest of THIS turn.
3. Call them directly. Do NOT look for `tools["<gateway>__mm-rag__…"]` inside
   `run_code` — that binding does not exist and hunting for it wastes turns.

**CONTEXT COST — read before fetching bytes.** With the relay, a tool result
enters the conversation verbatim, so `get_dataset_file_bytes` on a multi-hundred-
KB photo lands as base64 in context. Prefer a `_preprocessed` variant, cap the
size, and only fetch what you will actually render. The old "fetch inside the
program so the payload never reaches the model" trick required a programmatic
tool binding and no longer applies.

**Writing to disk.** Fetching bytes to a local file still needs a program that
receives them — call `get_dataset_file_bytes` directly and, if the payload is
small enough to carry, decode and write it with `run_code`.

Then render it with an **absolute local path** in markdown. The harness rewrites
absolute paths into its own authenticated file route (`api/file?path=...`) on its
own origin, so there is no cross-site fetch and no cookie problem:

```markdown
![description](/abs/path/to/photo.jpg)
```

Alternatively call the `read_image` tool on the written path — it attaches the
image inline and downscales automatically. Verified with a 16 MB source.

## Filenames: two forms, only one is stable

`get_dataset_files` returns the **stored** name with a 32-hex prefix:
`2f2e7aee2826424fa3feb7ad2560bdef_DSC01378_preprocessed.JPG`.

- `get_dataset_file_bytes` requires the **full stored name**.
- The REST file route also accepts the short name (uuid prefix stripped).
- The HMAC media token is bound to whichever form it was minted for — a token
  minted for the short name will NOT validate against the full path.

## The size ceiling

`get_dataset_file_bytes` refuses files over `MCP_FILE_BYTES_MAX` (default
**256 KiB**). There is **no offset/range parameter** — you cannot chunk around
it. The check is server-side and env-only.

Measured on a real 195-image dataset: 256 KiB covers 79%, 1 MiB 90%, 4 MiB 93%,
8 MiB 95%. The tail is brutal (largest was 29 MB), so pick a sane cap rather than
chasing 100%.

To raise it, set it via chart `extraEnv` (it reaches both containers):

```yaml
extraEnv:
  MCP_FILE_BYTES_MAX: "8388608"   # 8 MiB
```

Cost is **transient server RAM (~2x file size per call)**, not tokens — because
the bytes never enter context in PTC mode. Watch `MCP_POOL_SIZE`: worst case is
pool-size x 2 x cap of peak RAM.

## What NOT to do

- **Do not propose `--skip-auth-route` on the shared oauth2-proxy** to make URLs
  render. It is a platform-wide process: the flag weakens the pattern for every
  app behind that proxy, it strips the identity headers the app ACL check needs
  (`if identity is not None` — so the per-user dataset ACL silently stops being
  enforced), and it still does not make a tokenless URL work: the app then 403s
  because `registry_configured()` demands a token or password. If a gateway change
  is genuinely needed, ask for a host-scoped `notPaths` on that app only.
- **Do not set the cap to the largest file you can find.** Size it to the
  distribution you actually need and keep the pool-size multiplication in mind.
- **Do not build a local proxy** unless a *non-agent* consumer needs a stable URL
  (a browser tab, an external doc). For rendering inside an agent session,
  fetch-to-disk is simpler, has no listener to keep alive, and has no network
  dependency at all.

## Fast triage

| Symptom | Cause | Do |
|---|---|---|
| Image renders as bare alt text | cross-site Lax cookie, 302 to SSO | fetch bytes -> disk -> local path |
| `413` / "over the tool cap" | file > `MCP_FILE_BYTES_MAX` | raise via `extraEnv`, or use a `_preprocessed` variant |
| "File not found" for a name you can see | used the short name | use the full 32-hex-prefixed stored name |
| Token rejected (403) | token minted for the other filename form | re-mint against the exact path you request |
| Nothing renders, no error | gateway 302 returned HTML to an `<img>` | same as row 1 |