Repository navigation
Description
Filing category: ② a decision only the maintainer can make.
维护者速读
- 现象:报销明细的「电子发票」是必填文件字段。MCP 的
create_record没法带文件,被拒(「电子发票 is required」);create_upload_link只能给已存在的记录生成上传链接。先有鸡还是先有蛋:agent 无法通过 MCP 新增任何带必填文件字段的记录。附件通道(attachments: true)对普通成员也被拒(无sys_attachment创建权)。 - 要你裁:给 MCP 一条「先传文件、再建记录」的路,还是接受这类对象只能在界面里建。
- 推荐:A:
create_upload_link支持「为将要创建的记录」上传:返回一个文件令牌,create_record用这个令牌填文件字段(与界面上传后再提交表单是同一个顺序)。
One-sentence problem
"Help me file this taxi receipt" works in the console (pick the PDF, save) but cannot be done by an agent, because the agent can only ask for an upload after the record exists, and the record cannot exist without the upload.
Evidence (reach)
Hosted tenant (pnpm dev:stack --with-ui, Team plan; cloud ff373e02, framework d7c3d44e), member's MCP OAuth token (data:write):
create_record k7en_claim_line { …, invoice_no, amount }(no file) →电子发票 is required.create_upload_link { objectName: 'k7en_claim', recordId, attachments: true }→403 … your permissions do not allow creating attachments (sys_attachment).create_upload_link { objectName: 'k7en_claim_line', recordId: <existing line>, field: 'invoice_file' }→ link; opened in the member's browser, upload replaced the file ("Done. The file was attached"). So the link path works for an existing record only.read_fileon the line returned the PDF's text (invoice number, amount) — the read half is fine.
Governing text
- objectstack#30892 (closed): "MCP cannot read or attach files" —
read_file/create_upload_linkwere its answer; the create-with-required-file case was not covered. docs/NORTH-STAR.md— "files" is on the road; "Every step of the road must hold for both kinds of user".
Options
| Option | What it does | What the customer sees | Effort |
|---|---|---|---|
| A | create_upload_link without recordId (object + field) → the user uploads; the answer gives a short-lived file token the same user's create_record / update_record may put in that field |
Agent: "open this link and pick the invoice", then it files the line | storage + mcp seats · 1–2 PRs · precedent: the console's own upload-then-save |
| B | Let agents create the record with the file field empty when the caller will attach next (a "pending file" state) | Records exist briefly without their required file | data + mcp seats · 2 PRs · loosens required |
| C | Keep as is; document that objects with a required file are filed in the console | Agents cannot file expenses with invoices | none |
Business reading: A = the agent works like the form (file first, then save); B = the rule bends for agents; C = invoices are a human-only step.
Recommendation
A. It keeps required meaning required, mirrors how the console already orders upload and save, and keeps the token bound to the uploading user. Confidence gap: token lifetime and reuse across records — single-use, same user, same object/field, recommended; untested.
Self-check: only ① picks A; ②③④ do not flip it.
After the ruling
A: storage issues an unbound upload for (object, field, user); create_record accepts the token in that field; e2e: an agent files an expense line with a required invoice in two calls plus one browser upload.
os-decision-facets
① 项目长远合理性: A 复用「先上传、再保存」这一个顺序,不放宽 required,特例最小。
② 实际业务拉动: 报销、合同、证照等「必须附文件」的对象都卡在这一步(本次实测:报销明细)。
③ 防 AI 犯错: A 下文件与字段强绑定、单次、同一用户;B 让 agent 建出缺文件的记录,静默违反必填。
④ 创业阶段不扩散: A 给现有工具加一个模式;C 零成本但留下 North Star 缺口。
Prior rulings read: "create_upload_link required file field" → 2 hits (#30892, #31486); thread: none
Reader who acts: the storage seat and the packages/mcp seat, after the ruling.
Dedupe: repo:objectstack-ai/objectstack is:issue create_upload_link required file field → 2 hits (#30892 closed — the origin of the tool; #31486 QA run).
Activity
objectstack-fleet commentedon Nov 12, 2026
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH
Path: none
分诊首触 + 决策分析(fable 档独立评估)· 2026-11-12T16:54Z
维护者速读
是否已有裁决: 未找到。#30892 裁决 B(链接仅限已登录的铸造者本人)仍然约束本卡,A 与它不冲突。
前提核实:
create_upload_link只针对已有记录(packages/mcp/src/mcp-http-tools.ts:3283)。file-reference-lifecycle.ts,即界面的先传后存)。所以不需要新的令牌类型。四轴:
推荐 A,与 #32747 的 A 合成一次设计,顺序为 #32686 → #32747。置信缺口:MCP
create_record带本人未认领文件 id 时现有钩子是否接受,未实测;无记录时铸造链接要判什么权限,需契约评审。自检: 只看①选 A;不翻转。人工底线项:放宽上传门(仍须登录)。os-decision-facets
Prior rulings read: #30892 ruling B (6691366983); ADR-0104 D3; thread: none
Generated by Claude Code
objectstack-fleet commentedon Nov 13, 2026
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH
Brief-claimed-by: session_01Q4EjjMnyLqJdzuktEmRPXH · batch #1 · 2026-11-13T04:19Z
Decision batch #1 (the MCP agent-capability family, under epic #32897), taken on the maintainer's 「5张一批按总监决裁格式给我」.
objectstack-fleet commentedon Nov 13, 2026
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH
Ruling recorded · #32686 · A · 2026-11-13T04:40Z
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.needs-user-decision→pm:queue(domain:server, storage + mcp). Contract review covers the widening: which permission a pre-record link checks, and orphan-file reclamation. Acceptance: two agent calls plus one browser upload create an expense line with its required invoice.Generated by Claude Code
os-bob commentedon Nov 13, 2026
Re-grade on the maintainer's instruction:
priority:p2→priority:p0. State unchanged (pm:queue, unassigned). Written by a test session, not a seat.Blocked-by:it, and console AI chat: an uploaded image/PDF goes to the model and the agent puts that same file into the record's file field, as the user — ruled A #32749 (the console chat attaches the photo) waits on its attach step. Both were re-graded p0 in the same stroke.domain:serverlane claims and dispatches under its own identity.Generated by Claude Code
objectstack-fleet commentedon Nov 13, 2026
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH
Path: none
Triage: p0 → p1 · not an 18.4.0 blocker · 2026-11-13T07:55Z
⛔ Not a claim; the claim and state are unchanged, and the work continues.
mainor merge-queue red and the release window's blockers (「阻塞卡升 p0」). These six cards are MCP approval and upload capability work, not 18.4.0 regressions, so they carry p1: the highest working grade, first in their lanes after the blockers. That keeps the intent of the 07:27Z regrade (「…这些也都开始处理.这样我们审批相关的MCP就非常好用了.」) without holding the cut.Generated by Claude Code
objectstack-fleet commentedon Nov 13, 2026
triage#1 · hotlong · session_01Q4EjjMnyLqJdzuktEmRPXH
Path: none
Triage: H14 ·
pm:blockingderived · 2026-11-13T12:20Z⛔ Not a claim; grade and state unchanged. Open card(s) #32747 name this card on a line-leading
Blocked-by:, so the derived cache is added. It comes off when they close or drop the line.objectstack-fleet commentedon Nov 13, 2026
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx
Claim: PM loop round 1, card thread · 2026-11-13T14:28Z
Session:
session_01L9qibwGvonkT2ydXH6AKHmAccount:
os-teslaBranch:
claude/project-thread-vlkibiWorktree:
objectstack-issue-32686Domain:
domain:serverSeat:
domain:server#7File surface:
packages/services/service-storage/src/**(pre-record upload binding, the upload route, record file access),packages/mcp/src/mcp-http-tools.ts(create_upload_link,create_record,update_record),packages/mcp/src/skill-md.tsand its console mirrorui/apps/console/src/pages/developer/objectstack-skill.ts,packages/runtime/src/domains/mcp.ts, the create/update file-field write path only where the file id is accepted, a dogfood test underpackages/qa/dogfood/test/, at most one error-code ledger row inpackages/spec/src/api/error-code-ledger.zod.ts(spec carry-along, declared), root changeset. Stop on breach.Container & model:
L,mode:thread,model: default (dispatch-gates --tier: no path-derived mandate; PM default),effort: xhighReview: contract review owed before landing (the ruling names it: which permission a pre-record link checks, orphan-file reclamation), by the review lane thread or the seat
Clause-②: yes — a link without a record and a file id accepted in a required file field on create are new accepted call shapes
Metadata-protocol: none (expected; no metadata author key changes; the dev derives it)
Responsibility: n/a — not a defect card (ruled capability, A 6844207600)
Thread-read: 6852728166
Premise: holds (ruling A 6844207600 recorded after the card's last body edit; the dev re-measures the card's three readings at
origin/main2c98485dfirst)Serial constraints cleared: #32442 (
domain:server#5, branchclaude/project-thread-bb7kzf,mcp-http-tools.tsincl. lines nearcreate_record,runtime/src/domains/mcp.ts): written in parallel, landed serially, merge main after it lands; #32872 (this seat) sharesmcp-http-tools.ts/skill-md.tsin other regions, same rule; #32747 isBlocked-by:this card and is designed with it (shape left additive)objectstack-fleet commentedon Nov 13, 2026
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx
Claim amendment to this seat's claim 6855856900 on #32686 · 2026-11-13T15:00Z: the file surface adds two docs pages for this lane's packages, which go with the server seat (
lanes/server.md):content/docs/ai/connect-mcp.mdx, section "Files: read_file and create_upload_link": one paragraph on the two-call flow (a link for a new record, then the create naming the file id);content/docs/permissions/system-context.mdx, row 47e: the new caller-scoped verb, which refuses the system context like the other verbs listed there.Everything else in the claim stands.
objectstack-fleet commentedon Nov 13, 2026
domain:server#7 · os-tesla · session_01L9qibwGvonkT2ydXH6AKHm
os-dev-report
{ "issue": 32686, "status": "done", "branch": "claude/project-thread-vlkibi", "pr": "https://github.com/objectstack-ai/objectstack/pull/33123", "session": "session_01L9qibwGvonkT2ydXH6AKHm", "premise_still_valid": true, "summary": "create_upload_link takes objectName + field with no recordId where the storage service serves it: minting asks the object's create verdict, the field's writability and readonly; the answer carries fileIds (an array, so #32747 adds a count and more entries without a new key). The upload is judged against the field's accept/maxSize before the link is spent and becomes a committed file bound to (user, object, field, org, upload + 1h) in sys_file.metadata; applyCopyOnClaim refuses another user or a system write (unreadable-file words), another slot, a second record within the hour, and an expired claim, all as ADR-0114 field refusals; downloads of the new class are uploader-only while no record holds it. Docs: connect-mcp.mdx paragraph + system-context.mdx rows 47e and 47f (47f added beyond the amendment's 47e because its 'a system write claims any file' sentence would otherwise be false).", "tests": "At head a9a6589d (merged origin/main 7c36f524), under os-verify-lock (VERDICT command-exit 0 each): service-storage whole package Test Files 82/82, Tests 1540 passed; mcp whole package 63/63 files, 1104 tests; runtime src/mcp-record-file-bridge.test.ts 7/7; dogfood test/showcase-mcp-record-files.dogfood.test.ts 18/18 (3 new: two agent calls + one upload create dogfood_expense_line with its required invoice; another user's create refused + no download; no create permission → 403 PERMISSION_DENIED, nothing reserved). New pins: record-file-access.integration.test.ts 13 cases (incl. control: console-style presign/PUT/complete + save unchanged, no binding), pre-record-upload.test.ts 13, mcp-file-tools.test.ts 7, mcp-record-file-bridge.test.ts 2. Ablations via scripts/ablation-replace.mjs WRAP, committed first, each landed and restored to the HEAD blob: 19 legs; 17 red as expected (other user both arms, system write, other slot, spent, expired, uploader-only download, create verdict, writability, readonly, accept/maxSize before spend, page copy, reserved row uploader-only, unit not_yours arm, MCP optional schema, MCP no-record attachments, runtime fileIds, runtime member → dogfood acceptance red with 'expected string, received undefined at recordId'); 2 green by design (other-user integration pin with only one of its two arms removed: the read check refuses first). Logs: /mnt/project-files/server-seat-7/r2026-11-13/out/32686-evidence/.", "gates": "dispatch-gates --commands at the final diff: 106 families, each run with </dev/null, exit codes on disk before any pipe; plus the 4 roster gates under these paths (check:changeset-fixed, check:error-code-casing, check:filter-alias-parity, check:route-ledger-census) exit 0. Two exited 3 then 0: check-plugin-teardown-shape.mjs --self-test (fixture 621a4876 outside the shallow clone; fetched --depth=1) and check:dual-build-cjs-loads (PREREQUISITE no dist for 11 packages; 0 after check:type-check-debt's closure build, and 0 again at the merged head). --ran: '106 derived famil(ies) accounted for — 106 run, 0 NOT-MEASURED (a DERIVED zero)'. pnpm lint (whole tree) exit 0 at a9a6589d; eslint on 17 changed .ts exit 0; typecheck service-storage/mcp/runtime/dogfood exit 0 (runtime and dogfood need NODE_OPTIONS=--max-old-space-size=8192 here, default heap OOMs). CI: not yet run on this head; the seat reads the check-runs.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm; the touched dogfood file was run: showcase-mcp-record-files.dogfood.test.ts 18/18, lock VERDICT command-exit 0", "mcp_calls": "1 — mcp__github__pull_request_read (get #33074, read-only, to mirror a landed PR body); 0 writes", "api_writes": "2 — fleet relay: pr_create (objectstack-ai/objectstack#33123, run 43454921502, body read back identical, assignee os-tesla) and this os-dev-report comment on #32686 via post-stamped; git push is not a REST write", "open_questions": [], "security_findings": "withheld", "out_of_scope_findings": [ "class: a · reach: PUT /api/v1/storage/upload-links/:token on a link that names no record, then no create_record: the committed sys_file row (ref_id null) is still there after its hour (integration case 'expires' reads it back unclaimed) and no lifecycle rule nominates it, because sys_file's lifecycle reaps only deleted_at (30d) and status pending (7d); the same holds for a Console upload (presign/PUT/complete) never saved. Bytes are never reclaimed; after the hour the binding makes the file usable by no write. Evidence: objects/system-file.object.ts:306-307, stranded-orphan-inventory.ts header ('Committed rows carry neither trigger … so they are immortal'), verify-file-references.ts unreferenced_file advisory reports but does not reclaim · dedupe words: committed unclaimed sys_file immortal, unreferenced_file advisory reclaim, abandoned field upload, pre_record_upload orphan, stranded orphan inventory" ] }objectstack-fleet commentedon Nov 13, 2026
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx
Claim amendment to this seat's claim 6855856900 on #32686 (after 6856718179) · 2026-11-13T15:45Z: the file surface adds one sentence in
content/docs/api/plugin-endpoints.mdx(thePUT /storage/upload-links/:tokenrow), so it also describes a link that names no record. Docs page of this lane's package; everything else stands.objectstack-fleet commentedon Nov 13, 2026
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx
Claim amendment to this seat's claim 6855856900 on #32686 (after 6857811650) · 2026-11-13T16:32Z: the
content/docs/permissions/system-context.mdxentry covers row 47f as well as 47e. Row 47f's "a system write claims any file" became false for pre-record bound files under this card; the dev disclosed the edit, and the seat's contract review checked it. Everything else stands.