Repository navigation
fix(mcp,storage): say which upload link a lock refuses; close the upload-link relay's code - #33142
Conversation
…k relay's code create_upload_link's description and the connect-mcp page said a record a pending approval locks gets no link for any target; only a field link is refused, and an attachments link to a locked record is issued and attaches. The upload-link doors' refusal relay now answers the resolver's closed code, with an unregistered spelling beside it as declaredCode, as the commit door's relay does, and its docblock names all three declared-status limbs. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwbZycGiSpGfzuw3UC
…age code Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwbZycGiSpGfzuw3UC
📓 Docs Drift CheckThis PR changes 2 package(s): 12 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 5 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 19 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0b47c0de638d5602c97061ddf8641b219e2c4cab && git checkout 0b47c0de638d5602c97061ddf8641b219e2c4cab
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 840372560ad52ca7aaa522a0bfca9672d2011611 9292068c63b70dc5a8bb73e41474bfa1852e7688 && git checkout -B drift-repro 840372560ad52ca7aaa522a0bfca9672d2011611 && git merge --no-ff 9292068c63b70dc5a8bb73e41474bfa1852e7688
node scripts/docs-audit/affected-docs.mjs --json 840372560ad52ca7aaa522a0bfca9672d2011611
|
|
domain:server#7 · os-tesla · session_01L9qibzuZJBQY4wUQFJbkfx 维护者速读 · PR #33142 @
|
Requested by Tesla · project thread
Fixes #32783
Clause-②: no
Metadata-protocol: none
维护者速读
create_upload_link的工具说明和 connect-mcp 文档页,现在说清楚"锁定记录、只读字段不发链接"只针对字段目标;附件链接只问附件本身的两个问题,所以审批锁定中的记录照样能拿到附件链接并成功附上。另外,上传链接页面和上传接口转发拒绝时,error.code改为平台闭合错误码,应用自定义的未注册码放到旁边的declaredCode,与同文件提交接口的做法一致。packages/services/service-storage/src/storage-routes.ts的relayUploadLinkRefusal;packages/mcp/src/mcp-http-tools.ts的create_upload_link描述;content/docs/ai/connect-mcp.mdx的create_upload_link条目。pnpm --filter @objectstack/service-storage exec vitest run src/storage-routes.upload-link-refusal-relay.test.ts,预期 7 个全部通过。What changes
S1, the description says which link each refusal applies to. Measured at
origin/main67188849(RecordFileAccess.createUploadLinkAs,record-file-access.ts):mayEditAttachmentParent(PERMISSION_DENIED403); an attachments link asksassertMayAttach, thesys_attachmentcreate permission plus the same parent-edit verdict (ATTACHMENT_PARENT_ACCESS403).getWritableFields(FIELD_NOT_ACCESSIBLE403).readonlyfield gets no linkfieldDef.readonly === true(403).readonlyWhenholds now gets no linkengine.judgeFieldWriteLock(403, naming the lock).approvals.getRecordLock(RECORD_LOCKED409).sys_attachmentrow and thesys_filerow, never the parent, so the approvals lock (abeforeUpdateon the parent) never runs. Measured, see Verification.strictReadonlyWrites) refuses a field that became read-only, and the lock hook refuses a record locked after minting (RECORD_LOCKED409, pinned inrecord-file-access.integration.test.ts).createUploadLinkForNewRecordAsasks create permission, the writable field andreadonly;readonlyWhenis judged by the create.The description and
content/docs/ai/connect-mcp.mdxnow say the lock and the read-only refusals belong to a field link, and that an attachments link to a locked record is issued and attaches. "for this record and field only" now reads "for this record and this field (or its attachments) only". The served skill text (packages/mcp/src/skill-md.tsand its console mirror) does not restate the lock or the read-only refusals, so it is not changed.N2, the relay's code is closed.
relayUploadLinkRefusal(storage-routes.ts) answered the thrown spelling under anas ErrorCodecast. It now answersresolveThrownHttpError(err).code, withdemotedDeclaredCode(thrown)beside it asdeclaredCode, asrelayAttachFailurein the same file does. The ruling read: the module note ofpackages/types/src/thrown-http-error.ts, "[#9106] That limb was ruled (maintainer, 2026-08-16):error.codeis a closed vocabulary at every door". The 503 arm still relays only a code spelledSERVICE_UNAVAILABLE: it reads the spelling (thrown.declaredCode), because an unregistered spelling at 503 derivesSERVICE_UNAVAILABLEand reading the closed code would widen the arm.N3, the docblock names all three
declaredStatuslimbs:status, thenstatusCode, then the validation shape, which declares 400. It also says a throw with no stringcodeis not relayed.Tests
packages/services/service-storage/src/storage-routes.upload-link-refusal-relay.test.ts(new) mounts the storage family over a link door that throws a chosen refusal and drives the pageGETand the uploadPUT:declaredCode, on both doors (ablated);SERVICE_UNAVAILABLErelays (ablated);declaredCode(control);statusCode409RECORD_LOCKEDanswers 409;statusorstatusCode, answers 400VALIDATION_FAILED(N3);Acceptance notes
code. Every code the storage service's own link verbs throw is registered (ATTACHMENT_DOWNLOAD_DENIED,FIELD_NOT_ACCESSIBLE,FILE_DOWNLOAD_DENIED,FILE_NOT_FOUND,PERMISSION_DENIED,RECORD_LOCKED,RECORD_NOT_FOUND,RESOURCE_CONFLICT,RESOURCE_NOT_FOUND,SERVICE_UNAVAILABLE,VALIDATION_ERROR, each checked withErrorCode.safeParse), so their answers are unchanged. A refusal whose spelling is not registered, such as an application hook's own code on the upload's write, now answers the derived code inerror.codeand its own spelling inerror.declaredCode. The page's sentence does not change: every code it has its own sentence for is registered.record-file-access.tsmodule docblock still says areadonlyWhenlock "has no judgement a door can ask without writing, so it is judged when the link is used". Since Upload links: refuse at issue a field whose readonlyWhen holds now (carries the issue-time readonlyWhen half of #32734) #32768 minting asksjudgeFieldWriteLock. Comment drift only; noted, not filed.fieldshere. The relay answersVALIDATION_FAILED400 withoutdetails.fields, as before;relayAttachFailurepasses them. Not asked by the card; noted.mcp-http-tools.tsis shared with mcp: validate_expression still answers ok where the validation/formula gate refuses (null-guard, undeclared hop field, and the rest of the gate's private passes) #32872 (PR fix(mcp): validate_expression gives the publish gate's verdict at validation and formula sites #33134), objectql: narrow the phase-1 one-holder refusal from environment to namespace for webhook, connector and email_template (ADR-0150 child B, webhook family) #31699 and [Decision] Should an approver be able to see and decide their pending approvals over MCP? #32442; this diff touches only thecreate_upload_linkdescription.Verification
On head
9292068c(one merge oforigin/mainf054a0b3, no conflict) unless stated.Measurement at origin/main 6718884: a locked record, two targets
A throwaway case (not committed) in
record-file-access.integration.test.ts, the record locked through the fixture's lock answer and itsbeforeUpdatelock hook:The attachments link was issued and its upload landed a
sys_attachmenton the locked record; the field link was refused 409. The file was restored withgit checkout HEAD --, andgit diff HEADwas empty.Ablations (src, read directly by the suite)
Both ran through
scripts/ablation-replace.mjs, which proved the mutation landed (anchor 1 to 0, blob changed) and restored the blob (e1f86c0c…before and after).git diff HEADwas empty after each.sendError(res, status, thrown.code, …, declaredCode)put back tosendError(res, status, e.code as ErrorCode, message, { userMessage }). Red, 2 failed / 5 passed:GETandPUT"a 4xx refusal spelling an unregistered code answers a registered one",expected 'CLAIM_DESK_SAYS_NO' to be 'RESOURCE_CONFLICT'.thrown.declaredCode === 'SERVICE_UNAVAILABLE'replaced withthrown.code === 'SERVICE_UNAVAILABLE'. Red, 1 failed / 6 passed: "a 503 relays only as spelled SERVICE_UNAVAILABLE",expected 503 to be 500.Tests, typecheck, lint
pnpm --filter @objectstack/service-storage test: Test Files 83 passed (83), Tests 1554 passed (1554).pnpm --filter @objectstack/mcp test: Test Files 63 passed (63), Tests 1104 passed (1104).pnpm --filter @objectstack/service-storage --filter @objectstack/mcp typecheck: exit 0.pnpm lint: exit 0.Derived gates: 102 of 102, exit 0
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 102 commands from 5 paths; each ran with stdin from/dev/nulland its exit code was written to disk. Three first exited 3 (PREREQUISITE NOT MET:check:skill-examplesneeds the client SDK built,check:i18nthe CLI closure,check:dual-build-cjs-loadsa full build). Each exited 0 after the build it names.--ranwith every exit recorded: "102 derived famil(ies) accounted for — 102 run, 0 NOT-MEASURED (a DERIVED zero — all 102 recorded an exit code and none of them is 3)".check-nul-bytes: exit 0.Dogfood: n/a, since the title has no
!and Clause-② has no(narrowing)arm.