diff --git a/.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.i18n.yaml b/.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.i18n.yaml index 3f40fc0de8..ca30dccd30 100644 --- a/.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.i18n.yaml +++ b/.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.i18n.yaml @@ -2,5 +2,5 @@ # side as of the last confirmed-consistent state. Both languages carry equal authority; # after editing either side, bring the other along and re-record with: # pnpm run verify-translation-pairing --write .agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.md -2026-09-07-session-prose-local-media-display.md: b62b4d1e5c382540150fad3e2840e2f7bbe33dd2 -2026-09-07-session-prose-local-media-display.zh.md: 101f2d234b59e5efebd0f42fa39830747e9181fa +2026-09-07-session-prose-local-media-display.md: 79bde26e1890bf463f7a954e9b84d81e87ecbd1e +2026-09-07-session-prose-local-media-display.zh.md: af852a627888353589e4a88a4198bfb32760adbe diff --git a/.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.md b/.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.md index b62b4d1e5c..79bde26e18 100644 --- a/.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.md +++ b/.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.md @@ -14,7 +14,7 @@ Local media paths in session prose render through one same-origin file route, wi - **Renderer seam (`ui-primitives`)**: `MarkdownText` gained a `MarkdownPathImages` vocabulary (`pathImages` prop) with the same settled-only gate as `fileMentions`: while a message streams, frozen cached blocks never bake in a vocabulary handler; the settled pass rewrites image destinations that fail the remote-URL allowlist, and a rewritten destination is emitted only when it is an absolute `http(s)`/`blob`/`data` URL. Without a vocabulary the renderer output is byte-identical to before. - **Chat wiring (`ui-chat`)**: `AssistantMarkdown` supplies a page-stable vocabulary mapping absolute POSIX paths to same-origin `/api/file?path=…` GETs (`local-path-media.ts`); non-HTTP transports (Electron `file://`) and relative/protocol-relative destinations stay inert. -- **Host route (`session-controller`)**: `SessionMediaReferences`, a plugin contribution registered beside `SessionFileReferences`, mounts `GET|HEAD /api/file` on the shared authenticated `connection.fetch` channel (same trust fence and browser authentication as `/api` RPC). Per request it fail-closes: the path must be absolute, its `realpath` must lie inside a registered workspace root, the file must be regular, and its extension must name an allowlisted media type (PNG/JPEG/GIF/WebP/AVIF, MP4/WebM/MOV/OGG, MP3/WAV/Ogg/M4A/AAC/FLAC); media bytes are never sniffed on this route. Responses stream with HTTP range support (206/416) so video and audio can seek, and carry `private, no-store` and `nosniff`. The contribution activates only where `connection` and `workspaceRegistry` are composed (pending-until-composed, like the package's other optional contributions). +- **Host route (`session-controller`)**: `SessionMediaReferences`, a plugin contribution registered beside `SessionFileReferences`, mounts `GET|HEAD /api/file` on the shared authenticated `connection.fetch` channel (same trust fence and browser authentication as `/api` RPC). Per request it fail-closes: the path must be absolute, its `realpath` must lie inside a registered workspace root, the file must be regular, and its MIME type must belong to the served categories image/video/audio (resolved by the `mime-types` package, excluding `image/svg+xml`); media bytes are never sniffed on this route. Responses stream with HTTP range support parsed by the `range-parser` package (206/416) so video and audio can seek, and carry `private, no-store` and `nosniff`. The contribution activates only where `connection` and `workspaceRegistry` are composed (pending-until-composed, like the package's other optional contributions). The route is presentational and stateless: it never writes, follows no redirects, and returns 400/403/404/415/416 instead of approximating another file-serving behavior. diff --git a/.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.zh.md b/.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.zh.md index 101f2d234b..af852a6278 100644 --- a/.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.zh.md +++ b/.agents/notes/implemented/feature/2026-09-07-session-prose-local-media-display.zh.md @@ -24,7 +24,7 @@ Assistant 正文有时用本地文件系统路径引用图片(markdown `![](/U - **注册到 `workspace-controller`**:该包负责 workspace 注册表生命周期(CRUD、排序、feed),与文件呈现只共享 registry 这一政策数据源。否决并完整回滚。 - **经会话 RPC 取字节后显示 blob/data URL**:附件图片已如此工作,但 markdown 重写需要渲染时确定性同步 URL(流式冻结缓存、memo 化);异步往返不能成为渲染缝。否决。 - **逐图片或仅图片专用路由**:媒体类型共享同一条「路径 + 包含」政策,视频/音频本就需要 Range 流式;一条 `/api/file` 路由加扩展名 allowlist 即可覆盖现有与后续媒体类型。作为更窄的方案被否决。 -- **图片扩展的字节签名校验**:否决。相同检查已在 `fs/tool-fs` 的 `read_image` 工具内实现,仓库的跨文件克隆门禁止在此复制,而为单个辅助函数加宽 attachment 包公开 API 没有共享归属。扩展名 allowlist 已把非媒体内容挡在门外,损坏的图片载荷失败发生在浏览器侧而不是路由上。 +- **手写 MIME 表、Range 解析与图片签名校验**:否决,改用维护中的包(`mime-types`、`range-parser`)且不做嗅探。其中字节签名校验被否决的理由是:相同检查已在 `fs/tool-fs` 的 `read_image` 工具内实现,仓库的跨文件克隆门禁止在此复制,而为单个辅助函数加宽 attachment 包公开 API 没有共享归属。扩展名 allowlist 已把非媒体内容挡在门外,损坏的图片载荷失败发生在浏览器侧而不是路由上。 - **每请求交互授权、客户端协商端点或任意 Host 路径**:重写只是呈现;路由对每次请求复验,可读字节限制在注册 workspace 根与 allowlist 媒体内;同源端点是固定通道契约而非协商能力。出于安全与确定性否决。 ## Consequences diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index 1be29ddbaf..c0000c969f 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -89,11 +89,13 @@ External packages that a workspace package resolves at runtime. The tier covers | [`micromark-util-sanitize-uri`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-sanitize-uri) | MIT | | [`micromark-util-symbol`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-symbol) | MIT | | [`micromark-util-types`](https://github.com/micromark/micromark/tree/main/packages/micromark-util-types) | MIT | +| [`mime-types`](https://github.com/jshttp/mime-types) | MIT | | [`negotiator`](https://github.com/jshttp/negotiator) | MIT | | [`node-addon-require-builtin`](https://www.npmjs.com/package/node-addon-require-builtin) | MIT | | [`node-pty`](https://github.com/microsoft/node-pty) | MIT | | [`open`](https://github.com/sindresorhus/open) | MIT | | [`picomatch`](https://github.com/micromatch/picomatch) | MIT | +| [`range-parser`](https://github.com/jshttp/range-parser) | MIT | | [`react`](https://github.com/facebook/react) | MIT | | [`react-dom`](https://github.com/facebook/react) | MIT | | [`readable-stream`](https://github.com/nodejs/readable-stream) | MIT | @@ -152,9 +154,11 @@ External packages **directly declared** only by repository tooling, test infrast | [`@types/fs-ext`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/js-yaml`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/jsdom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | +| [`@types/mime-types`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/negotiator`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/node`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/picomatch`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | +| [`@types/range-parser`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/react`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/react-dom`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | | [`@types/readable-stream`](https://github.com/DefinitelyTyped/DefinitelyTyped) | MIT | diff --git a/packages/api/session-controller/package.json b/packages/api/session-controller/package.json index 93846093fe..2b3d08e98a 100644 --- a/packages/api/session-controller/package.json +++ b/packages/api/session-controller/package.json @@ -71,6 +71,8 @@ "@deepseek-ai/dsh-brand": "workspace:^", "@deepseek-ai/dsh-deque": "workspace:^", "@deepseek-ai/schemastery": "workspace:^", + "mime-types": "^3.0.2", + "range-parser": "^1.3.0", "zod": "^4.4.3" }, "peerDependencies": { @@ -122,8 +124,8 @@ "@deepseek-ai/dsh-attachment": "workspace:^", "@deepseek-ai/dsh-client-connection": "workspace:^", "@deepseek-ai/dsh-client-file-upload": "workspace:^", - "@deepseek-ai/dsh-commands": "workspace:^", "@deepseek-ai/dsh-client-store": "workspace:^", + "@deepseek-ai/dsh-commands": "workspace:^", "@deepseek-ai/dsh-file-reference": "workspace:^", "@deepseek-ai/dsh-jobs": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", @@ -146,6 +148,8 @@ "@deepseek-ai/dsh-util-crypto": "workspace:^", "@deepseek-ai/dsh-util-time": "workspace:^", "@deepseek-ai/dsh-util-workspace-path": "workspace:^", - "@deepseek-ai/dsh-workspace": "workspace:^" + "@deepseek-ai/dsh-workspace": "workspace:^", + "@types/mime-types": "^3.0.1", + "@types/range-parser": "^1.2.7" } } diff --git a/packages/api/session-controller/src/media-references.ts b/packages/api/session-controller/src/media-references.ts index 42e225cbdc..3130b52050 100644 --- a/packages/api/session-controller/src/media-references.ts +++ b/packages/api/session-controller/src/media-references.ts @@ -10,121 +10,67 @@ * - its canonical location must lie inside a registered workspace root * (`ctx.workspaceRegistry`); no other directory is readable; * - the file must exist and be a regular file; - * - its extension must name an allowlisted media type (media bytes are never - * sniffed here: the allowlist keeps non-media content out, and browsers - * already reject corrupt image payloads); - * - responses are private, uncached, sniff-proof, and support HTTP range - * requests so `