Skip to content

storage + mcp: one upload link on a multiple: true file field accepts several files (same user, record, field, one use, same expiry) — ruled A #32747

Closed

Description

@os-bob

triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

The signature line above and the Blocked-by: line at the end were added by triage#1 when recording the ruling; the card text below is the filing seat's, unchanged.

Filing category: ② a decision only the maintainer can make.

维护者速读

  • 现象:用户把 3 张发票发给 AI,AI 能读懂并通过 MCP 建好报销单,但发票文件本身只能走 create_upload_link:AI 给用户 3 个链接,用户在浏览器里把同样的 3 个文件再各选一次。一个链接只收一个文件,哪怕字段是 multiple: true。
  • 要你裁:① 一个链接能否一次传多个文件;② 能读本地文件的 AI 客户端(Claude Code、桌面代理)能否把文件内容直接交给 MCP(base64),不用再让人选一遍。
  • 推荐:A(只做①:多文件字段的链接一次可选多个文件),② 暂不开。

One-sentence problem

"Here are my three invoices, file the claim" costs the user three link clicks and three re-picks of files they already gave the agent.

Evidence (reach)

Hosted tenant (pnpm dev:stack --with-ui, Team plan; cloud ff373e02, framework d7c3d44e), member's MCP OAuth token, claim object with invoices: { type: 'file', multiple: true }:

  • create_record → draft claim; create_upload_link { field: 'invoices' } ×2 → each link takes one file and appends it (record-file-access.ts: value = [...fileIdsIn(current[field]), fileId]); read_file (with fileId) reads each back; run_action submits. Measured end to end.
  • packages/mcp/src/mcp-http-tools.ts create_upload_link: "uploads one file"; no MCP tool takes file content.

Governing text

  • objectstack#30892 (closed): "An AI client sees an image the user attaches in its own chat, but it has no bytes to send. The MCP protocol has no standard client-to-server file transfer." → upload link, ruled option B (signed-in user only).
  • That premise holds for chat clients; it does not hold for clients that read local files (they can send base64 in a tool argument).

Options

Option What it does What the customer sees Effort
A On a multiple: true field, one link accepts several files (same user, same record and field, one use, same expiry) "Open this link and pick your 3 invoices" — once storage seat · 1 PR · precedent: the console form already takes several files at once
B A + an attach_file tool taking { objectName, recordId, field, filename, contentBase64 }, size-capped (e.g. 5 MiB), stored as the calling user and marked as added by an AI client Claude Code / desktop agents attach directly; chat clients still use the link storage + mcp seats · 2 PRs · widens the write surface
C Keep as is One link per file none

Business reading: A removes the repetition for everyone without a new door; B removes the re-pick for agents with file access but lets an agent put bytes the user never picked onto a record (an agent could generate a plausible "invoice" PDF), so provenance becomes the reviewer's problem.

Recommendation

A. It cuts the cost of the common case (several invoices) with no new trust question. Confidence gap: whether B's convenience is worth the provenance question for expense-type records; recommend revisiting B when a customer workflow needs agent-side bulk attach.
Self-check: ① and ③ pick A; ② (business pull) would pick B for Claude Code users; ④ picks A.

After the ruling

A: the upload-link page takes multiple on a multi-file field, appends each file in one write, e2e: one link, three PDFs, three files on the field.

os-decision-facets
① 项目长远合理性: A 沿用已裁定的链接通道,只放宽"一个链接一个文件"。
② 实际业务拉动: 报销一次常有多张发票(本次实测 2 张需 2 个链接);能读本地文件的 agent 用户更想要 B。
③ 防 AI 犯错: B 让 agent 能写入用户没亲手选的文件(可伪造发票);A 仍由人选文件。
④ 创业阶段不扩散: A 一个 PR;B 新增写入工具和来源标记。
Prior rulings read: #30892 (upload link, option B sign-in only); thread: none

Reader who acts: the storage seat (upload-link door) and the packages/mcp seat, after the ruling.
Dedupe: repo:objectstack-ai/objectstack is:issue mcp upload base64 file content → 9 hits, QA runs only; upload link multiple files one link → 22 hits, none on this; mcp attach file bytes agent local file → 258 hits, none on this.

Blocked-by: #32686

Activity

  1. added
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 12, 2026
  2. os-bob commented on Nov 12, 2026

    @os-bob
    ContributorAuthor

    Clarification from the user who raised this: the real scenario is a photo of the invoice sent to the AI in a chat app. There the client has no bytes to forward, so option B does not help this user at all; only A (one link, several photos picked from the phone album at once) reduces their steps. This strengthens the recommendation for A and leaves B for agents with local file access only.

    Not yet measured: a phone photo (JPEG) through the link (the PDF path is measured). The field's accept list has no .heic; iOS usually converts to JPEG when a browser file input picks from Photos, but that is unverified here.


    Generated by Claude Code

  3. os-bob commented on Nov 12, 2026

    @os-bob
    ContributorAuthor

    Measured for option B (2026-11-12, same tenant): an agent running on the user's machine with the invoice photo as a local file (JPEG). It reads the photo (amount ¥268.00, meal, invoice number), files the claim over MCP (create_record → draft with amount 268), and then has no way to attach the photo: no MCP tool takes file content, and the same MCP OAuth token on POST /api/v1/storage/upload/presigned answers 401 AUTH_REQUIRED (correct — the token is bound to the MCP resource). Its only route is an upload link, so the user re-picks a file the agent already holds.

    So the business pull for B is real for local agents (Claude Code, desktop agents), not hypothetical. Updated recommendation: A now, B next, B with the provenance mark (added by an AI client, which client), the size cap, and the same edit-door check as the link. The forged-file concern is unchanged and is the reason B carries the mark rather than being refused.


    Generated by Claude Code

  4. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    分诊首触 + 决策分析(fable 档独立评估)· 2026-11-12T16:55Z

    维护者速读

    • 3 张发票要发 3 个链接、选 3 次。
    • ① 一个链接一次选多个文件:沿用您已裁的「须登录的链接」通道。
    • ② agent 直接交文件内容(base64):这是一道新的写入门,agent 能放上用户没亲手选的文件,比如伪造的发票。
    • 建议这次只裁 ①。请裁:A 一链多文件 / B A + agent 直传 / C 不做。推荐 A。

    是否已有裁决:

    前提核实:

    • 一条 pending sys_file 行对应一个链接、一个文件。A 要改已评审过的「单次使用」条件,工作量比卡上写的 1 个 PR 高。
    • 第一方 multiple: true 的文件字段为 0。
    选项 客户看到 工作量
    A 多文件字段的链接一次收多个(同一用户、记录、字段,设上限) 打开链接一次选 3 张发票 storage · 1–2 个 PR + 契约评审;先例 #31009
    B A + attach_file(base64) 本地 agent 直接挂上;聊天软件里发照片的场景无益 2 个以上 PR;无先例;新写入面
    C 不做 一个文件一个链接 0

    四轴:

    • ① 长远:一次请求多个文件是通行做法;B 的来源真实性问题对财务凭证很重。
    • ② 拉动:多张发票是常态。
    • ③ 防 AI:A 仍由人选文件;B 的来源标记只是事后披露。
    • ④ 不扩散:A 改现有门,B 新增工具和标记。

    推荐 A,与 #32686 合成一次设计。置信缺口:手机相册 HEIC 格式未实测;上限数值待定。自检: 只看①选 A;不翻转。人工底线项:B(新写入门)。

    os-decision-facets

    • ① 长远:沿用已裁的须登录链接,只放宽容量;B 的来源真实性未解
    • ② 拉动:多张发票常见(实测 2 张发票用了 2 个链接);B 只利好本地 agent
    • ③ 防 AI:A 由人选文件;B 让 agent 能写入用户没选的字节
    • ④ 不扩散:A 改现有门,须契约评审;B 新增工具与标记,暂缓

    Prior rulings read: #30892 ruling B and its Ask 3 (deferred, not refused); ADR-0104 D3; thread: 2 comments, no ruling


    Generated by Claude Code

  5. added
    area:filesFiles — upload, download, signed URLs, access derived from the parent record
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 12, 2026
  6. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    eval#1 · os-bob · session_012dn2AcTB69pEwZ9ugr1g1w

    More reach for ② (agent holds the bytes) — and a case on a child-table file field · 2026-11-13T03:05Z

    Measured on the os-dev-test environment today, with a third-party local agent (WorkBuddy; models DeepSeek-V4.1-Flash / GLM-5.3-Flash) operating an OA app over MCP for its user.

    The case. The user handed the agent two invoice PDFs for a travel expense claim. The app models the claim as a header (c982_expense_claim) with line items (c982_expense_item, master_detail → claim); the invoice is a file field on each line item, one invoice per line, so the approver checks each amount against its own invoice.

    • The agent read both PDFs (amounts ¥329.00 / ¥277.00, order numbers, invoice numbers), rebuilt the claim and its two lines over MCP — all fields filled.
    • It also hashed the files and noticed that two of the user's files for one invoice were the same file (MD5 equal), so it de-duplicated before filing. It holds the bytes.
    • To attach them it can only call create_upload_link once per line item → two links, each bound to one child record + field, ~10 minutes. It then told the user: 「我这边的 OA 连接没有『直接推送文件』的能力……链接必须在你自己登录 OA 的浏览器里打开,选择本地 PDF 上传」. The user re-picks two files the agent already has, one link per line.

    Two things this adds to the card.

    1. Option A does not help this shape. A is "one link takes several files on a multiple: true field". Here each invoice belongs to a different child record, so it is still one link per line no matter how many files a link accepts. N line items = N links = N re-picks. Only B (an agent that holds the bytes hands them over) removes the round trip for line-item attachments, which is the normal shape of an expense claim.
    2. Ordering trap with approvals. The app locks line items while the claim's approval is pending (a validation reading record.claim.approval_status, so the approver reviews exactly what was submitted). The agent had to stop and ask the user to upload before submitting — "一旦提交进入审批中,明细会被锁定,发票就传不进去了". With links that expire in minutes and a human in the loop, a missed upload means recall → upload → resubmit (and recall is itself not available to agents yet, MCP: a submitter's agent cannot recall or resubmit its own approval request — approval_recall / approval_resubmit are not ai.exposed, while the approver's four decide actions now are (ruling A on #32442) #32993).

    Neither changes the security concern triage raised for B (an agent can attach a file the user never picked). It does mean the business pull for B covers local agents on master–detail forms, where A gives no relief.


    Generated by Claude Code

  7. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Brief-claimed-by: session_01Q4EjjMnyLqJdzuktEmRPXH · batch #1 · 2026-11-13T04:20Z

    Decision batch #1 (the MCP agent-capability family, under epic #32897), taken on the maintainer's 「5张一批按总监决裁格式给我」.

  8. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Ruling recorded · #32747 · A · 2026-11-13T04:41Z

    Maintainer, verbatim (triage#1 session): 「以上原则同意」, answering decision batch #1 (32641 B / 32591 A / 32592 C / 32686 A / 32747 A). The batch was analysed independently at the fable tier, with prior rulings checked first; the full analysis was presented in the session.


    Generated by Claude Code

  9. added
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    and removed
    needs-user-decisionBlocked on a maintainer decision — do not dispatch; decision cards only, never on a PR
    on Nov 13, 2026
  10. added
    priority:p0Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial
    and removed on Nov 13, 2026
  11. 1 remaining item

  12. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    Triage: p0 → p1 · not an 18.4.0 blocker · 2026-11-13T07:57Z

    ⛔ Not a claim; the claim and state are unchanged, and the work continues.


    Generated by Claude Code

  13. added
    priority:p1High: required for production / M2
    and removed
    priority:p0Queue-jump: outranks batch and breaks the round — never exempts claiming or same-file serial
    on Nov 13, 2026
  14. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH

    Path: none

    Triage: unblocked → pm:queue · 2026-11-13T18:46Z

  15. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Nov 13, 2026
  16. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

    Claim: PM loop round 8, card thread · 2026-11-13T19:53Z · p1, ruling A 6844223218
    Session: session_01L9qibzVFJSNs6XZjC8jkNJ
    Account: os-tesla
    Branch: claude/project-thread-4i7bf9
    Worktree: objectstack-issue-32747
    Domain: domain:server
    Seat: domain:server#7
    File surface: packages/services/service-storage/src/ (upload-link store, storage-routes.ts link PUT door, pre-record-upload.ts, record-file-access.ts, upload-link-copy.ts and the page), packages/mcp/src/mcp-http-tools.ts (the create_upload_link description and answer), packages/mcp/src/skill-md.ts + ui/apps/console/src/pages/developer/objectstack-skill.ts, content/docs/ai/connect-mcp.mdx, content/docs/permissions/system-context.mdx rows that describe link use, their tests, a dogfood test, one root changeset. ⛔ No base64 input, no new MCP tool, no packages/spec change without stopping first. Stop on breach.
    Container & model: L, mode:thread, model: default (dispatch-gates --tier: no path-derived mandate; PM default), effort: high
    Clause-②: yes (widening) — a link on a multiple: true field accepts several files, each answered with its own id; a single-file field link and an attachments link stay one file
    Metadata-protocol: none
    Responsibility: the one-file limit sits in the upload-link door in service-storage and is stated by create_upload_link in packages/mcp (objectstack) | the console form already takes several files at once; #32686 already answers fileIds as an array | every MCP user attaching several files to a multi-file field reaches it today
    Thread-read: 6861932019
    Premise: holds (measured end to end in the card's evidence; #32686 merged since, so the dev re-measures at origin/main first)
    Serial constraints cleared: mcp-http-tools.ts is shared with this seat's #32872 (PR #33134), #32530 and #31699; storage-routes.ts last changed by merged PR #33142; edits stay on the upload-link paths; land serially, merge main after a sibling lands

  17. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 13, 2026
  18. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx

    Claim amendment to this seat's claim 6863322469 on #32747 · 2026-11-13T19:57Z: the file surface adds

  19. objectstack-fleet commented on Nov 13, 2026

    @objectstack-fleet
    Contributor

    domain:server#7 · os-tesla · session_01L9qibzVFJSNs6XZjC8jkNJ
    os-dev-report

    {
      "issue": 32747,
      "status": "done",
      "branch": "claude/project-thread-4i7bf9",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/33151",
      "session": "session_01L9qibzVFJSNs6XZjC8jkNJ",
      "premise_still_valid": true,
      "summary": "A link for a `multiple: true` file field takes up to UPLOAD_LINK_MAX_FILES (10) files in one multipart submit (count + file0..fileN-1); each file gets its own id and the field gains them all in one write, or none (any store or the write failing discards every file of the send). A link for a record not yet created takes `count` (1-10, >1 only on a multi-file field) and answers that many fileIds, the extras reserved as pending rows refused to every write before the upload. Single-file field links and attachments links stay one file with unchanged refusals; success answers gain the additive `fileIds`. create_upload_link gains optional `count` (no new tool); the page takes `multiple`; skill-md, connect-mcp.mdx and system-context row 47f updated; the Console skill mirror needs no change (no-scope skill, no file tools). Head 6a6df23affbd19a8d4842f8746eed3e78e005ebd.",
      "tests": "service-storage vitest: 83 files / 1566 tests pass (record-file-access.integration.test.ts 63, 13 new under #32747). mcp vitest: 63 files / 1109 pass (5 new). dogfood showcase-mcp-record-files.dogfood.test.ts 21/21 (3 new: one link three PDFs, count 3 + create_record, size refusal of one of three). Ablations via scripts/ablation-replace.mjs, each restored with git diff HEAD empty: count check dropped (3 red), pre-spend accept/maxSize only for pre-record (1), pre-record discard last only (1), record discard last only (2), reserved rows committed (4), same-user check dropped (2), single-file link takes 10 (6), mint count>1 on single-file (1), mint cap dropped (1), form part checks off (1), MCP count+recordId passed through (1), size limit first file only in rebuilt dist (dogfood 1 red: 200, three stored). Typecheck service-storage, mcp, runtime, dogfood: exit 0.",
      "gates": "dispatch-gates --commands at 6f09953b: 106 commands + 4 artifact-roster gates under changed dirs (check:changeset-fixed, check:error-code-casing, check:filter-alias-parity, check:route-ledger-census), each with </dev/null, exits on disk: 110 exit 0. check:docs (exit 1, stale json-schema after the merge), check:skill-examples (exit 3) and check:dual-build-cjs-loads (exit 3) rerun after pnpm --filter @objectstack/spec build: exit 0. --ran: 106 derived, 106 run, 0 NOT-MEASURED, 0 UNRUN. pnpm lint: exit 0. CI check-runs on the PR: not yet reported.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm; the touched dogfood file was run (21/21)",
      "mcp_calls": "5 — issue_read (get x1, get_comments x4); 0 write tools",
      "api_writes": "0 REST proxy writes. Relay writes: 2 (pr_create #33151 with assignees os-tesla; this os-dev-report comment). git pushes to claude/project-thread-4i7bf9 only.",
      "open_questions": [],
      "security_findings": "withheld",
      "out_of_scope_findings": []
    }
  20. changed the title [-][Decision] Should an AI client be able to attach a file it already holds without the user re-picking it through an upload link (and may one link take several files)?[/-] [+]storage + mcp: one upload link on a multiple: true file field accepts several files (same user, record, field, one use, same expiry) — ruled A[/+] on Nov 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:filesFiles — upload, download, signed URLs, access derived from the parent recorddomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p1High: required for production / M2

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions