Skip to content

feat(kap-server): support server-local path attachments - #3247

Open
7Sageer wants to merge 2 commits into
mainfrom
feat-web-attach-by-path
Open

feat(kap-server): support server-local path attachments#3247
7Sageer wants to merge 2 commits into
mainfrom
feat-web-attach-by-path

Conversation

@7Sageer

@7Sageer 7Sageer commented Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Related Issue

None — internal feature (problem described below).

Problem

In web/desktop sessions, attached files go through the browser File API, which only exposes name + bytes. The daemon therefore materializes every upload into the session attachments dir and the agent only ever sees that copy path — never the file's original location. Consequences: agents asked to edit an attached file edit the throwaway copy, large files are stored twice, and the model-facing notice text (Attached file "..." ... — open it with the Read tool) doubles as the UI's attachment record, which web clients must regex-parse back into chips.

What changed

Attach by server-local path (zero-copy). The prompt API now accepts file parts with a server-local absolute path (exactly one of file_id/path), and image/video sources with kind: 'path'. Validation: absolute, realpath-resolved (symlink-safe), not on the Read tool's sensitive-file list, and local runtime only (checked against the target agent's runtime binding before any fs probing). Path-sourced files keep their original path in the byte-identical model-facing notice — no copying. Path images reuse the existing compression pipeline (caption names the original path, originals are not re-persisted) and are capped by the existing MAX_IMAGE_DECODE_BYTES; path videos stream into the daemon store like uploads.

Typed attachment metadata on the prompt origin. UserPromptOrigin gains attachments ({name, mediaType, size, path}), populated at submit for every resolved file part, merged on steer, and surfaced on turn.started as a new kind: 'file' prompt-attachment variant. Live (coreEventMap) and cold (groupTurns) transcript projections emit attachment entities with mediaType/name/size and no sourcesource.kind is a closed union parsed by shipped clients, so no new enum values are introduced; clients keep recovering the path from the notice text they already parse.

packages/protocol mirrors the new wire shapes (schema + XOR constraint); the submit route's OpenAPI errors declare FILE_NOT_FOUND.

Review-hardening included (external model review): realpath before sensitive-check, guard ordered before fs stat (no existence probing via error codes), resolve-stage fs errors mapped to file.not_found.

Deferred (pre-existing #3088-chain gaps, not regressions): steered/hook-blocked prompts don't re-project live attachments; undo doesn't retract live attachment entities.

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (none — internal feature).
  • I have added tests that prove my feature works: kap-server (path intake, byte-identical notice, zero-copy, symlink/sensitive/relative/oversize rejection, non-local runtime guard ordering, skills route), agent-core-v2 (turn.started attachments, steer merge), transcript (cold-rebuild fold), protocol (schema XOR).
  • Ran gen-changesets skill, or this PR needs no changeset.
  • Ran gen-docs skill, or this PR needs no doc update.

Web and desktop clients can now attach files, images, and videos to a
prompt by server-local absolute path instead of uploading a copy. The
daemon validates the path (absolute, realpath-resolved, non-sensitive,
local runtime only) and references the original file in place, so the
agent reads the original path; the upload flow is unchanged.

Submitted file attachments are also recorded on the prompt origin and
projected as typed transcript attachments, so web clients render
attachment chips for plain files without parsing the model-facing
notice text.
@changeset-bot

changeset-bot Bot commented Aug 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0eaa85d

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

@pkg-pr-new

pkg-pr-new Bot commented Aug 25, 2026

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

commit: 0eaa85d

@7Hanrui

7Hanrui commented Aug 26, 2026

Copy link
Copy Markdown

@codex review

@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

attachmentParts.push(...contentToCoreParts(preparedMedia.content));

P2 Badge Forward file metadata from skill activations

When the new skill-activation path receives a file attachment, resolvePromptMediaFiles returns its typed metadata in preparedMedia.attachments, but this route forwards only the converted text notice. Since turnPromptAttachments can recover file attachments only from prompt origin metadata, these files produce no attachment entity or chip in either the live projection or cold rebuild; pass the metadata through the skill activation prompt origin as the normal prompt route does.

ℹ️ 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 (part.type === 'file') {
if (part.path !== undefined) {
const sourcePath = part.path;
const { info } = await statAttachmentFile(sourcePath);

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 Recheck sensitivity on the realpath used for intake

If a symlink is retargeted after assertPromptPathRefs runs but before media preparation, this second realpath can resolve to a sensitive file without another isSensitiveFile check; image/video branches then read that file directly, while the file branch publishes the mutable symlink path for a later Read. Validate the same resolvedPath that is subsequently read and retain that canonical path in file notices and metadata.

Useful? React with 👍 / 👎.

@7Hanrui

7Hanrui commented Aug 26, 2026

Copy link
Copy Markdown

@codex review

@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: 0eaa85d40f

ℹ️ 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".

ownedFileIds.add(saved.id);
content.push({
type: 'image',
source: { kind: 'url', url: buildDaemonFileUrl(saved.id) },

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 IDs on path-sourced media parts

When a client is already subscribed to the live transcript and submits a path-sourced image, this generated image_url omits id (and the video branch does the same), so turnPromptAttachments rejects it because it requires the URL's file ID to equal the explicit id. Consequently the live projector emits no attachment for the turn, although a later cold rebuild recognizes the daemon URL and restores it; include id: saved.id on both generated media references so live and cold transcripts agree. Transcript subscribers cannot recover this from the legacy event because projected session events are suppressed for them.

AGENTS.md reference: packages/kap-server/AGENTS.md:L29-L29

Useful? React with 👍 / 👎.

Comment on lines +355 to +356
.catch((error: unknown) => {
if (isFsError(error)) throw fileNotFoundError(sourcePath);

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 Do not convert destination write failures into 404s

When copying a path-sourced video into the daemon store fails because the destination is full, read-only, or otherwise unavailable, the catch covers the entire store.save(...) operation and isFsError converts errors such as ENOSPC or EACCES into FILE_NOT_FOUND. The source can still exist, so clients receive a misleading 404 and cannot distinguish a server storage failure; only map errors raised while reading the source path and let destination/store failures propagate as server errors.

Useful? React with 👍 / 👎.

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