Repository navigation
fix(mcp,runtime)!: the MCP data tools refuse to filter, sort, group or aggregate on an internal field, with the data door's own 400 INVALID_FIELD - #28772
Conversation
…ld evaluation gate before the engine Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
…o the data door's internal-field refusal Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
…member object read on the fixture Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
…ontext Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
…ernal-field-query
📓 Docs Drift CheckThis PR changes 3 package(s): 5 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 40 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 859f8cf0d676571ef50c104a1c6fb8b712778c54 && git checkout 859f8cf0d676571ef50c104a1c6fb8b712778c54
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 72533449d3ef11c67594ed05c8a1d4074ef432e0 79470662fc7329acd6106ed74b23ef12a3bf1bd1 && git checkout -B drift-repro 72533449d3ef11c67594ed05c8a1d4074ef432e0 && git merge --no-ff 79470662fc7329acd6106ed74b23ef12a3bf1bd1
node scripts/docs-audit/affected-docs.mjs --json 72533449d3ef11c67594ed05c8a1d4074ef432e0
|
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk Contract reviewServed-tier: Narrowing face ( ① Derived judgments
② Semver level
ADR-0087: exactly one marker, ③ Boundary flags
④ Design judgmentOn the narrowing itself against the published spec on Should-fix (non-blocking):
Implemented-by: VERDICT: PASS |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk ACCEPT → head 维护者速读
席位意见:收。本 PR 落实维护者 2026-11-01 的裁定(a,原话「同意」);达档契约复核 6562332618 PASS,没有必须修改的项。 席位核对
Generated by Claude Code |
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk
Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no file under
skills/**teaches filtering, sorting, grouping or aggregating on aninternal: truefield over MCP;content/docs/api/error-catalog.mdxalready states that a read naming such a field in an evaluate position is refusedINVALID_FIELD, which this diff makes true on the MCP data tools too.维护者速读
query_records/aggregate_records,HTTP/mcp的aggregate_records)在交给引擎之前,改为调用 REST 数据入口同一个「internal: true字段不可求值」闸门(协议服务上的assertNoInternalFieldEvaluated,改为公开方法,定义仍只有一份);对这类字段做过滤、排序、分组或聚合,统一回400 INVALID_FIELD,文案与 REST 逐字相同。internal: true的字段做过滤/排序/分组/聚合的调用;普通字段、行读取(该字段仍不出现在任何行里)、字段权限的 403 均不变;未注册协议服务的宿主照旧。回滚 = revert 本 PR,无数据迁移。packages/mcp/src/stdio-data-bridge.ts的refuseInternalFieldEvaluation及其在query/aggregate中的两处调用;②packages/runtime/src/action-execution.ts中callData的aggregate分支新增段;③packages/metadata-protocol/src/protocol.ts中assertNoInternalFieldEvaluated去掉private及其文档段。pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=1 test/mcp-internal-field-evaluate-positions.dogfood.test.ts,预期 32/32 通过(28 个拒绝用例与 REST 的 code/status/message 逐字一致,4 个普通字段对照用例照常返回)。What changes
The data door refuses to EVALUATE a field declared
internal: true— a filter, a sort, a group key, an aggregation operand or an aggregation's own filter on it — for every caller, before the engine is asked (assertNoInternalFieldEvaluated,@objectstack/metadata-protocol). Three MCP mouths hand a caller's query to the engine without passing throughfindData, where the door asks that gate, so they evaluated those positions. Each now asks the door's ONE gate of theprotocolservice it already resolves, and answers what the door answers, word for word.@objectstack/metadata-protocol—assertNoInternalFieldEvaluatedlosesprivate; its docblock names the MCP callers. No other change: one definition, no copy beside it (route-ownership rule 1).@objectstack/mcp, stdio transport —refuseInternalFieldEvaluationasks the protocol's face;queryasks it withwhereandorderBy,aggregatewithwhere,groupByandaggregations, after the stored-metadata refusals (the door's order) and before the engine. The bridge header's "known divergences" gains this limb.StdioProtocolObjectReadsdeclares the optional face.@objectstack/runtime, HTTP bridge —callData'saggregatearm (the/mcpaggregate_records) asks the same face beforeengine.aggregate. Thequeryarm already reachesfindData, unchanged.403 PERMISSION_DENIEDfor a field the caller may not read; the HTTP bridge'squery_records(it already met the gate). A host that registers no protocol service has no data door to agree with and is served as before — see Acceptance notes.Tests
packages/metadata-protocol/src/protocol.data-door-internal-field-positions.test.ts— new block: for each canonical query the MCP mouths can send (filter$eq/$startsWith, sort, aggregatewhere, group key, operand, aggregation filter, relation condition), the public face throws exactly whatfindDatathrows (code, status, object, field, param, message); ordinary fields pass it.packages/runtime/src/action-execution-calldata-aggregate-internal-field.test.ts—callData('aggregate')over the REALObjectStackProtocolImplementationas theprotocolservice: each position × member / admin / system answersfindData's own refusal and the engine never aggregates; controls: ordinary fields reach the engine unchanged, and a host with no protocol is served as before.packages/mcp/src/stdio-data-bridge.internal-field-evaluation.test.ts— the bridge asks the face with the call's own arguments before the engine, the refusal reaches the caller unchanged (and renders as the tool'scode/status/ message envelope), the engine is never asked; no protocol, or one without the face: served as before.packages/qa/dogfood/test/mcp-internal-field-evaluate-positions.dogfood.test.ts— the real stack: showcase with its security andMCPServerPlugin, a runtime object with aninternal: truefield authored throughPUT /meta/object, one stdioMCPServerPluginper principal started withautoStartand itsOS_MCP_STDIO_API_KEYminted throughPOST /keys. Seven positions × {stdio, HTTP/mcp} × {admin, member}:isError,INVALID_FIELD,400, and the message equal to whatPOST /data/:object/queryanswers for the same body in the same run. Controls per transport and principal: an ordinary field filters (narrowing one row of two), sorts, aggregates and groups; the flagged column is absent from every served row.Acceptance notes
protocolservice (registerProtocol: falseand noMetadataProtocolPlugin) has no data door, so the gate has no verdict to share there and the MCP tools serve as before; the engine still refuses a group key or an aggregation operand on such a field (rejectCredentialAggregation). Both mouths say so in their comments; this PR does not make that composition refuse. Raised as an open question in the report.internalfields); the stdio bridge's own relation-target exposure gate still answers first, as before.Verification
Final head
79470662f— the branch withorigin/main(6cbdef1e4) merged in; the merge touches none of this diff's files. Readings are from this worktree (objectstack-ai/objectstack), every exit code captured before any pipe, heavy runs underscripts/pm/os-verify-lock.sh.Targeted pins — on 7947066, all exit 0
@objectstack/metadata-protocolsrc/protocol.data-door-internal-field-positions.test.ts: Tests 50 passed (50).@objectstack/runtimesrc/action-execution-calldata-aggregate-internal-field.test.ts: Tests 17 passed (17).@objectstack/mcpsrc/stdio-data-bridge.internal-field-evaluation.test.ts: Tests 5 passed (5).@objectstack/dogfoodtest/mcp-internal-field-evaluate-positions.dogfood.test.ts: Tests 32 passed (32).Touched packages — full suites and typecheck, all exit 0
@objectstack/metadata-protocolon 7947066 (the merge brought changes into this package, so it was re-run there): Test Files 292 passed, 4 skipped (296); Tests 21963 passed, 26 skipped (21989);typecheckexit 0.@objectstack/mcpon8e5daeeca(the merge changed nothing underpackages/mcp): Test Files 49 passed (49); Tests 617 passed (617);typecheck(with its test layer) exit 0.@objectstack/runtimeon8e5daeeca(the merge changed nothing underpackages/runtime), local project: Test Files 505 passed (505); Tests 8203 passed, 19 skipped (8222);typecheck(with its test layer) exit 0. Therepoproject is CI's (Test Core).@objectstack/dogfoodtypecheckexit 0 (8e5daee). ESLint over the six changed TypeScript files on 7947066: exit 0.Dogfood battery — on 7947066
pnpm --filter @objectstack/dogfood exec vitest run --maxWorkers=1, closure built first, under the verify lock: lock VERDICT command-exit 0; Test Files 372 passed, 3 skipped (375); Tests 3026 passed, 28 skipped (3054); no failure.One-shot ablation — on 7947066, cited here and not kept as a test
scripts/ablation-replace.mjs(anchor must hit): the stdio bridge's gate call andcallData's gate call each replaced by aconsole.debugmarker — anchor 1 to 0, marker 0 to 1, blob changed, for both files; on-disk counts re-read inside the wrap.@objectstack/mcpand@objectstack/runtimerebuilt;scripts/ablation-dist-preflight.mjsfound each marker in the consumeddist/(2 built files each).aggregate_recordsrows —whereand the aggregation's own filter were SERVED; group key and operand answered in the engine's words, not the door's. Green: the 6 HTTPquery_recordsrows (they reachfindData) and the 4 controls.git diff HEADis empty (both checked by the tool and again by the script); both packages rebuilt;--absentpreflight: markers absent from every built file, working tree clean.Gates — derived by dispatch-gates at 7947066
node scripts/pm/dispatch-gates.mjs --commands: 66 commands. Run with exit codes recorded;--ranreconciliation: 66 derived, 65 run, 1 NOT-MEASURED, 0 UNRUN.check:dual-build-cjs-loads, reason: exit 3 PREREQUISITE NOT MET — it reads every package'sdist/and this worktree built the dogfood closure only. CI's run is the reading.check:type-check-coverageandcheck:type-check-debtran inside the same hold (their closure build nested in it): exit 0,--re-measure0 raw tsc errors.check:dts-closurefirst answered exit 1 (@objectstack/metadata-coredeclarations missing) while that--re-measurewas rebuilding the closure in the same hold; re-run on the settleddist/: exit 0, 241 of 241 declared declaration files present.check:slot-lookupwas red on the first sweep (anany-typed stub plugin context in the new dogfood file); fixed in8e5daeeca, green since.check-durability-degradation-log-level.mjs,pm/check-half-states.mjs) changed onorigin/mainafter this merge; the local readings are of this tree's copies.Generated by Claude Code