Skip to content

feat(web): allow attaching any file type and fix CSP on non-loopback binds - #1731

Merged
wbxl2000 merged 13 commits into
MoonshotAI:mainfrom
sailist:feat/web-file-upload-csp
Jul 15, 2026
Merged

feat(web): allow attaching any file type and fix CSP on non-loopback binds#1731
wbxl2000 merged 13 commits into
MoonshotAI:mainfrom
sailist:feat/web-file-upload-csp

Conversation

@sailist

@sailist sailist commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

No linked issue — the problems are explained below (from user reports of remote / SSH-accessed server deployments).

Problem

Two web/server gaps surfaced when the Kimi server is accessed remotely (server on a remote host, browser on a local machine):

  1. The web chat cannot attach non-media files, and unsupported image formats vanish. The composer only accepted image/*,video/*; anything else was silently filtered out. Worse, images in formats model providers reject (e.g. SVG) were dropped with an [Image omitted] notice, so the model could not see the user's content at all and started guessing (e.g. searching the filesystem for the file).
  2. The Content-Security-Policy breaks web UI assets on non-loopback binds. The CSP stamped on non-loopback binds (default-src 'self') blocks the inline anti-FOUC bootstrap script in index.html ('self' never covers inline scripts) and the data: fonts baked into dependency CSS (KaTeX, Inter/JetBrains Mono Variable). This is the same root cause as fix(web): allow image sources in CSP #1672, which only covered img-src; the script/font gaps remained. The OAuth login loop observed in the same report is a separate issue already fixed by fix(agent-core-v2): keep invalidated OAuth flows observable during login #1663 + fix(web): show errors for failed actions and add daemon request/operation logging #1711.

What changed

Commit 1 — feat(web): allow attaching any file type in chat

  • Composer / paste / drop no longer filter out non-media files; arbitrary files upload as generic icon chips and are submitted as file content parts (wire schema already supported them).
  • kap-server materializes file parts into the session's attachments/ directory and replaces them with a path reference (Attached file "<name>" (<media_type>, <size> bytes): <path> — open it with the Read tool), so the model reads the file on demand instead of receiving inline bytes. This mirrors the existing <video path="..."> convention.
  • Images rejected by the provider format gate (SVG, AVIF, …) — both uploaded and inline base64 — are now persisted and referenced by path instead of dropped, so e.g. an uploaded SVG stays readable (URL-source images keep the text notice; there are no bytes to persist).
  • Uploaded file names are sanitized (path separators, control chars, leading dots, length cap) before hitting disk.

Commit 2 — fix(server): stop CSP from blocking web bootstrap script and fonts

  • Moved the anti-FOUC bootstrap from an inline <script> in index.html to /boot.js — a classic render-blocking script with identical timing, which script-src 'self' (via default-src) covers without hashes/nonces/'unsafe-inline'. A regression test asserts the served index.html carries no inline scripts or inline event handlers, so the invariant cannot silently regress.
  • Added font-src 'self' data: for the dependency-baked font data URIs.
  • Set explicit form-action 'self', base-uri 'none', and frame-ancestors 'self' — these directives do not fall back to default-src and were previously unrestricted.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue, or explained the problem above.
  • I have added tests that prove my feature works.
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

@pkg-pr-new

pkg-pr-new Bot commented Jul 15, 2026

Copy link
Copy Markdown
pnpm dlx https://pkg.pr.new/@moonshot-ai/kimi-code@a77285f
npx https://pkg.pr.new/@moonshot-ai/kimi-code@a77285f

commit: a77285f

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 76e28d13c2

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

name: file.name,
kind,
previewUrl,
mediaType: file.type,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve upload MIME metadata for unknown files

When the selected file has an empty File.type (common for extensionless or custom file types, which this change now allows), this stores mediaType: '' on the attachment. After upload, the chip only gets fileId, so submitPromptInternal later serializes media_type: att.mediaType ?? 'application/octet-stream'; the empty string bypasses that fallback and the prompt schema rejects it because media_type must be non-empty. Users can therefore upload these arbitrary files but cannot send the prompt; normalize empty MIME strings here or copy the server-returned result.mediaType onto the attachment.

Useful? React with 👍 / 👎.

@changeset-bot

changeset-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: a77285f

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@moonshot-ai/kimi-code Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@wbxl2000

Copy link
Copy Markdown
Collaborator

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ed91757f4d

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +1337 to +1341
else if (att.kind === 'file') {
content.push({
type: 'file',
fileId: att.fileId,
name: att.name ?? '',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve generic file attachments in submitted turns

For non-media uploads this optimistic user message now stores a { type: 'file' } part, but messagesToTurns only renders file parts when their media type starts with image/ or video/ (apps/kimi-web/src/composables/messagesToTurns.ts:701-704), and ChatPane otherwise displays only turn.text. A PDF-only prompt therefore appears as an empty user bubble after submit, and edit/resend reloads no attachment; for steered prompts the same shape also fails to match the server echo, because the server converts file parts into a plain text attachment notice. Add a generic rendered attachment shape instead of relying on AppMessageContent.file here.

Useful? React with 👍 / 👎.

@wbxl2000
wbxl2000 force-pushed the feat/web-file-upload-csp branch from ed91757 to cb5892e Compare July 15, 2026 06:55
@wbxl2000

Copy link
Copy Markdown
Collaborator

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb5892e208

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

for (const att of attachments ?? []) {
if (att.kind === 'video') content.push({ type: 'video', source: { kind: 'file', fileId: att.fileId } });
else content.push({ type: 'image', source: { kind: 'file', fileId: att.fileId } });
else if (att.kind === 'file') {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve generic files in rendered user turns

When the user sends a non-media attachment (e.g. a PDF/Makefile), this new file part becomes the only record in the optimistic user message; however messagesToTurns only converts file content into turn.images for image/video MIME types, and the undo/edit flow rehydrates only turn.text plus turn.images. A file-only send therefore renders as an empty bubble and Undo/Edit loses the attachment, so generic files need to be carried through the rendered turn or represented as text/chips before storing the optimistic message.

Useful? React with 👍 / 👎.

sailist and others added 9 commits July 15, 2026 20:06
- Composer, paste, and drop no longer filter out non-media files;
  arbitrary files upload as generic icon chips and are submitted as
  file content parts
- kap-server materializes file parts into the session's attachments
  dir and replaces them with a path reference, so the model opens the
  file with the Read tool on demand instead of receiving inline bytes
- Images rejected by the provider format gate (SVG, AVIF, ...) are now
  persisted and referenced by path instead of being dropped with a
  notice; uploaded file names are sanitized before hitting disk
- Move the anti-FOUC bootstrap from an inline <script> in index.html
  to /boot.js: CSP 'self' never covers inline scripts, while a classic
  same-origin script keeps the same render-blocking timing
- Allow data: in font-src — KaTeX and the Inter / JetBrains Mono
  Variable fonts ship @font-face data URIs in their distributed CSS
- Set explicit form-action, base-uri, and frame-ancestors, which do
  not fall back to default-src
- Add a regression test asserting the served index.html carries no
  inline scripts or inline event handlers
Files with an empty File.type (Makefile, LICENSE, other extensionless
or unknown types) stored mediaType: '' on the chip, and the submit
fallback used ?? which does not catch empty strings — the wire schema
requires a non-empty media_type, so the prompt was rejected. Normalize
to application/octet-stream at attachment creation, adopt the
server-recorded MIME after upload completes, and make both submit
mappings use || so reloaded chips with '' are covered too.
@wbxl2000
wbxl2000 force-pushed the feat/web-file-upload-csp branch from b16f85b to c493057 Compare July 15, 2026 12:07
@wbxl2000

Copy link
Copy Markdown
Collaborator

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4930575c9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +49 to +50
if (previewable && win !== null) {
win.location.href = url;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Force active attachments to download

When a clicked attachment is text/html or image/svg+xml (both are classified as previewable above), this navigates user-controlled bytes as a blob URL created by the Kimi Web origin. Active content opened this way is not sandboxed/noopener-isolated, so a malicious HTML/SVG attachment can execute in a same-origin tab and reach the opener or same-origin API state after the user clicks the chip. Treat active document types as downloads, or open them through a sandboxed/noopener viewer that renders them inert.

Useful? React with 👍 / 👎.

Comment on lines +503 to +505
content.push({
type: 'text',
text: buildAttachedFileNotice(file.meta.name, file.meta.media_type, file.meta.size, attachedPath),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve generic attachments across reloads

For non-media file submissions this stores only a text notice in the prompt history, while the client renders sent file chips only from type: 'file' content. The optimistic bubble looks correct immediately after send, but after a page reload/resync or prompt-list hydration the same turn comes back as plain text (Attached file ...) with no file id, so the attachment can no longer be clicked, downloaded, or loaded back into the composer. Keep UI metadata for the file part or teach the client to recover it from the stored representation.

Useful? React with 👍 / 👎.

wbxl2000 added 2 commits July 15, 2026 21:06
Clicking a file chip navigated a new tab to a blob: URL of the uploaded
bytes whenever the type looked browser-renderable. blob: inherits the
web origin, so a text/html or image/svg+xml attachment would execute
same-origin script with the daemon credential (localStorage) and a live
window.opener.

Preview is now restricted to inert types (pdf, non-SVG images, video,
audio, non-HTML text), the blob is re-wrapped with the whitelisted MIME
instead of trusting the recorded content-type, and window.opener is
severed. Non-whitelisted types no longer silently download: the chip
reports 'unsupported' and the pane shows a transient hint.
The kap-server prompt route replaces file parts with an "Attached file
…" text notice before enqueueing, so after any snapshot resync the
attachment chip degraded into raw notice text leaking the absolute
server path — unlike image/video uploads, which already recover their
chip from the <video|image path> tag. Parse the notice the same way:
the materialized basename carries the file id, so the chip becomes
clickable again (and editable back into the composer); inline-base64
notices (content-hash named, no file id) still collapse into a
non-clickable chip instead of raw text.

The notice wording is now a client/server contract — flagged on
buildAttachedFileNotice.
@wbxl2000

Copy link
Copy Markdown
Collaborator

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 763440c2d1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

const m = ATTACHED_FILE_NOTICE_RE.exec(text.trim());
if (!m) return null;
const base = (m[4] ?? '').split(/[\\/]/).at(-1) ?? '';
const id = base.slice(0, base.indexOf('-'));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve UUID file IDs when rebuilding attachment chips

When the notice is for a normal uploaded file, the basename is emitted as <file.meta.id>-<name>, but file IDs are generated from randomUUID() and contain hyphens (for example f_...-...). Splitting at the first - leaves only the first UUID segment, so FILE_STORE_ID_RE rejects it and, after a reload/resync, the rebuilt generic-file chip has no fileId/URL and cannot be opened or resent as the original file. Parse using an unambiguous delimiter or recover the full ID another way.

Useful? React with 👍 / 👎.

Comment on lines 264 to 265
if (!uploadImage()) return;
e.preventDefault();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Ignore non-file drops in the document handler

When uploads are enabled, this document-level drop handler runs for every drop event, not just file drops; unlike dragenter/dragover, it does not check windowDragHasFiles before preventDefault(). Dropping selected text into the composer or dropping text/links into another input will be swallowed with an empty file list. Gate the drop path on file items before preventing the default action and calling addFiles.

Useful? React with 👍 / 👎.

@wbxl2000

Copy link
Copy Markdown
Collaborator

@codex

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d8e5fcc20f

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

* unsent draft attachments (mirroring loadForEdit(text), which overwrites) so
* a later submit sends exactly the edited message's files, not a mix. */
function loadAttachments(atts: { fileId?: string; kind: 'image' | 'video'; url: string; name?: string }[]): void {
function loadAttachments(atts: { fileId?: string; kind: 'image' | 'video' | 'file'; url: string; name?: string }[]): void {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid re-uploading empty URLs for file chips

When the server notice comes from an inline-base64 unsupported image, messagesToTurns rebuilds a kind: 'file' attachment with no fileId and url: ''. Because loadAttachments now accepts file attachments, undo/edit passes that chip here and it falls into the existing no-fileId re-upload path, where fetch('') resolves to the current page and uploads the web app HTML; resending the edited prompt then replaces the original attachment with the wrong bytes. Skip file attachments without a real URL/fileId, or preserve them as non-resendable, instead of trying to fetch the empty URL.

Useful? React with 👍 / 👎.

@wbxl2000

Copy link
Copy Markdown
Collaborator

@codex

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Keep them coming!

Reviewed commit: a77285f994

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

@wbxl2000
wbxl2000 merged commit 0b790cd into MoonshotAI:main Jul 15, 2026
14 checks passed
@github-actions github-actions Bot mentioned this pull request Jul 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants