feat(web): allow attaching any file type and fix CSP on non-loopback binds - #1731
Conversation
commit: |
There was a problem hiding this comment.
💡 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, |
There was a problem hiding this comment.
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 detectedLatest commit: a77285f The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
There was a problem hiding this comment.
💡 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".
| else if (att.kind === 'file') { | ||
| content.push({ | ||
| type: 'file', | ||
| fileId: att.fileId, | ||
| name: att.name ?? '', |
There was a problem hiding this comment.
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 👍 / 👎.
ed91757 to
cb5892e
Compare
There was a problem hiding this comment.
💡 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') { |
There was a problem hiding this comment.
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 👍 / 👎.
- 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.
b16f85b to
c493057
Compare
There was a problem hiding this comment.
💡 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".
| if (previewable && win !== null) { | ||
| win.location.href = url; |
There was a problem hiding this comment.
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 👍 / 👎.
| content.push({ | ||
| type: 'text', | ||
| text: buildAttachedFileNotice(file.meta.name, file.meta.media_type, file.meta.size, attachedPath), |
There was a problem hiding this comment.
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 👍 / 👎.
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.
There was a problem hiding this comment.
💡 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('-')); |
There was a problem hiding this comment.
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 👍 / 👎.
| if (!uploadImage()) return; | ||
| e.preventDefault(); |
There was a problem hiding this comment.
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 👍 / 👎.
There was a problem hiding this comment.
💡 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 { |
There was a problem hiding this comment.
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 👍 / 👎.
|
Codex Review: Didn't find any major issues. Keep them coming! Reviewed commit: ℹ️ About Codex in GitHubCodex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
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". |
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):
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).default-src 'self') blocks the inline anti-FOUC bootstrap script inindex.html('self'never covers inline scripts) and thedata: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 coveredimg-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 chatfilecontent parts (wire schema already supported them).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.Commit 2 —
fix(server): stop CSP from blocking web bootstrap script and fonts<script>inindex.htmlto/boot.js— a classic render-blocking script with identical timing, whichscript-src 'self'(viadefault-src) covers without hashes/nonces/'unsafe-inline'. A regression test asserts the servedindex.htmlcarries no inline scripts or inline event handlers, so the invariant cannot silently regress.font-src 'self' data:for the dependency-baked font data URIs.form-action 'self',base-uri 'none', andframe-ancestors 'self'— these directives do not fall back todefault-srcand were previously unrestricted.Checklist
gen-changesetsskill, or this PR needs no changeset.gen-docsskill, or this PR needs no doc update.