Repository navigation
feat(spec)!: an authored connector entry refuses a top-level key it does not declare; the connector read withholds URL credential parts and a non-object auth - #28688
Conversation
…ls and non-object auth on read Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
…or-undeclared-keys Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 168 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 725d5decd4bdf004b382ebf4d3403440cefe0d17 && git checkout 725d5decd4bdf004b382ebf4d3403440cefe0d17
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin da6f0ce35da60bf81e79890b227815a989f9b165 91b48bc4b77feb5981bab7fdd2a8643f4d9268a6 && git checkout -B drift-repro da6f0ce35da60bf81e79890b227815a989f9b165 && git merge --no-ff 91b48bc4b77feb5981bab7fdd2a8643f4d9268a6
node scripts/docs-audit/affected-docs.mjs --json da6f0ce35da60bf81e79890b227815a989f9b165
|
…he connector entry root is closed Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk Co-authored-by: Claude <noreply@anthropic.com>
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: Taken under Generated by Claude Code |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk Contract reviewServed-tier: Read at 2026-11-01T16:07Z from the fetched branch ( ① Derived judgments
② Semver level
③ Boundary flagsThe PR author is
④ Design judgment
Must-fixes: none. Should-fixes (follow-up cards, none blocking): ③ (1) Implemented-by: VERDICT: PASS |
|
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk ACCEPT → head 维护者速读
席位意见:收。本 head 已过达档契约复核 6560432824( 席位核对
Generated by Claude Code |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review claim withdrawn · 2026-11-01T16:27Z⛔ Not a claim. The lane's record 6560432824 is on the same head ( A security-class follow-up on the connector read path is handled outside GitHub under the 2026-10-29 disclosure ruling. It does not block this PR, because the gap is not introduced by this diff. Generated by Claude Code |
domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — every connector entry
skills/**,content/docs/**,examples/**andpackages/apps/**author (eachdefineConnector({…})andconnectors: [{…}]literal, scanned) already uses only declared entry keys, so nothing they teach changes; the new refusal carries its own prescription.维护者速读
connectors:/PUT /meta/connector/:name)改为封闭结构,未声明的顶层键在 defineStack、os validate/compile、构建产物加载与保存时按键名拒绝(保存为 422);连接器的元数据读取额外隐去providerConfig下 URL 的 userinfo 与凭据类 query 参数,以及非对象形态的auth/authentication。auth.credentialRef,provider 配置挪进providerConfig);插件registerConnector的运行时形状不变。读出的 URL 去掉凭据部分后原样保存,存储值保留、运行时照常使用。回滚即 revert 本 PR。packages/spec/src/integration/connector.zod.ts(DeclarativeConnectorEntryBaseSchema与connectorBaseShape)、packages/spec/src/integration/connector-credential-redaction.ts(credentialPathsOf的非对象分支与credentialUrlRewritesOf)、packages/spec/src/kernel/metadata-authoring-lint.test.ts(最后一个开放根关闭,下限改为 0)。pnpm --filter @objectstack/dogfood exec vitest run test/connector-meta-door-entry-and-url-hardening.dogfood.test.ts,预期 9 个用例全部通过。What changes
Write door. An authored connector entry (
DeclarativeConnectorEntrySchema) is closed. It refines a dedicatedstrictObjectcarrier,DeclarativeConnectorEntryBaseSchema, built from the same shape declaration as the open base (connectorBaseShape(), called once per carrier so each has its own field instances and the guidance census never pairs the openConnectordef with the closed declaration). Every door that parses the entry refuses a top-level key the shape does not declare, by name, with a locatedunrecognized_keysissue:defineStack({ connectors }),os validate/os compile, the metadata plugin's artifact load at boot and reload, andPUT /api/v1/meta/connector/:name, where the answer is422 INVALID_METADATA. Two curated families ride the refusal: credential-shaped keys point atauth.credentialRef, and provider settings written at the top level (baseUrl,spec,transport,headers, …) point atproviderConfig; a few near-miss spellings (config,timeout,retry) are aliases to the declared key. The ADR-0010 protection envelope stays declared, so a package-stamped entry still parses. The refusal is terminal, so a body with an undeclared key meets that one refusal and not the ADR-0097 refinements as well.ConnectorSchema, the def a plugin hands toregisterConnectorand the shapedefineConnector()parses, keeps its posture; a control pin says so.Read redaction. The built-in
connectorredactor withholds two more positions:providerConfig(a string starting withscheme://) is served without its userinfo and without its credential query parameters (the datasource names plus the HTTP conventions such asapi_key,access_token,token,key,sig, and theauth.paramNameanapi-keyinstance names), the rest of the URL byte for byte. That coversbaseUrl,transport.urland a URLspec. The query half goes through the sameredactUrlCredentialQueryParamsthe datasource redactor uses, which now takes an optional name list (default and existing callers unchanged).authorauthenticationvalue that is not an object is withheld whole. Cleared forms (null,'') are served as written.Each rewritten or withheld position is named in
redactedKeys, so the protocol's carry-forward keeps the stored value on a save of the served form, and the runtime keeps reading the unredacted execution view.ADR-0087. A D3 semantic entry,
connector-entry-undeclared-key-refused, carries the prescription; there is no mechanical rewrite, because an undeclared key may be a provider setting whose home depends on the provider, or the only copy of a secret.Ledgers. The strictness ledger's
integration/row is re-classed from a flatwiretomixed, with the reason and the governing texts read before the close (ADR-0097, ADR-0143, ADR-0104, the type-schema suite's #2657 fence; none keeps the authored entry open). The authoring lint's last open root wasconnectors: its coverage floor moves to 0, asserted deliberately with the departed root confirmed rejected by the parse, and its two cross-collection fixtures move to a nested strip site (connectors[].retryConfig).Tests
packages/spec/src/integration/connector-entry-undeclared-keys.test.ts(new, 12 cases): the refusal for a scalar, a nested container and two keys on an instance and on a descriptor, pinned bycode,pathandkeys, never echoing the value; both prescriptions; terminality; the stamped envelope; a retired key keeping its own prescription and the retired-default residue; the stack door; and the openConnectorSchemaas a control.packages/spec/src/integration/connector-credential-redaction.test.ts(+15 cases): non-objectauth/authenticationwithheld whole, cleared forms served; userinfo, a bare user-part token, case-insensitive query parameters,transport.url, a URLspec, a relativespecleft alone, the instance's ownparamName, untouched non-credential URLs, arrays and dot-spelled keys off the walk, and a withheld map not walked twice.packages/qa/dogfood/test/connector-meta-door-entry-and-url-hardening.dogfood.test.ts(new, 9 cases, showcase boot): WRITE refusals (422 INVALID_METADATA, the key named, nothing stored,404to a member); CONTROL (declared-keys-only instance saves, reachesready, dispatches with the reference resolved); READ for a member and an administrator on the item, layered and list reads (both spellings), with the stored rows untouched; CARRY-FORWARD (a save of the served form keeps the stored URL whole, and dispatch sends it).packages/spec/src/kernel/metadata-authoring-lint.test.ts: the graduation above.Ablation, one leg per fix, each with on-disk mutation proof and restore proof (
scripts/ablation-replace.mjs: anchor count, blob change, blob back to HEAD, emptygit diff HEAD), and for the dogfood legs a spec rebuild withscripts/ablation-dist-preflight.mjsproving the marker reacheddist/and was gone after the restore rebuild:auth/authenticationnot withheldA first combined dist leg is not counted: its URL-rewrite marker was constant-folded by the bundler and the preflight could not find it in
dist/, so that leg was redone per fix with a string-literal marker.Acceptance notes
422envelope locates an unknown top-level key at the entry root (path: '') and names it in the message, as zod locatesunrecognized_keysat the containing object on every closed type.os buildwrote carries no undeclared key (it serializes the parsed stack), so only a hand-assembled artifact can meet the refusal at boot.metadata-type-schemas.test.ts's two notes that described the connector as open are dated rather than deleted; the suite's fence (envelope only, no posture case) is unchanged.Verification
Local runs on head
406ef4017(merge oforigin/mainat6b9c1ca99), each under the shared verify lock, verdict lines quoted:Package suites (all green)
pnpm --filter @objectstack/spec testcommand-exit 0pnpm --filter @objectstack/metadata-protocol testcommand-exit 0pnpm --filter @objectstack/metadata testcommand-exit 0pnpm --filter @objectstack/service-automation testcommand-exit 0pnpm --filter @objectstack/objectql testcommand-exit 0pnpm --filter @objectstack/runtime testcommand-exit 0pnpm --filter @objectstack/service-datasource testcommand-exit 0(first attempt99, never acquired; second attempt ran)pnpm --filter @objectstack/cli exec vitest run --project unitcommand-exit 0(theintegrationtier is declared to CI: the diff touches no spawn entry or integration file)pnpm --filter @objectstack/dogfood typechecktsc --noEmitcleancommand-exit 0pnpm --filter @objectstack/dogfood exec vitest run test/connector-meta-door-entry-and-url-hardening.dogfood.test.ts test/connector-meta-door-credential-hardening.dogfood.test.tscommand-exit 0(pre-merge head14e25f5de; the full battery below reruns both on this head)The connector packages (
connector-rest,connector-openapi,connector-mcp) are not in the diff and consumeConnectorSchema, which does not move; their suites are left to CI.Head is now
91b48bc4b, one commit after406ef4017. That commit is test(cli): it moves the #22238 probe key inpackages/cli/test/package-union-readers.test.tsto a nested strip site (retryConfig), because the connector root is now closed. CI Test Core (6/8) on91b48bc4bran that file: 7 passed, cli 137 files / 1718 passed.pnpm --filter @objectstack/spec typecheck: NOT MEASURED locally. Six attempts on the contended verify lock all answered99. CITypeScript Type CheckandType Check · workspaceare success on91b48bc4b.pnpm --filter @objectstack/dogfood teston91b48bc4b(the!title owes it):os-verify-lockVERDICT command-exit 0, Test Files 370 passed | 3 skipped (373), Tests 2990 passed | 28 skipped (3018). CI's Dogfood Regression Gate skips the dogfood legs onpull_requestby design, so this local battery is the evidence.91b48bc4b: 56 success, 9 skipped, 0 failure.Derived gate families (
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, merge base004871f79): 85 derived, 84 run with exit 0, 1 NOT MEASURED (pnpm check:dual-build-cjs-loads, exit 3, PREREQUISITE NOT MET: seven packages outside this diff have nodist/in this worktree);--ranreconciliation: 85 accounted, 0 unrun.pnpm --filter @objectstack/spec check:generatedis among the 84 and is green aftergen:strictness-ledger(theintegration/shard total moves 3 → 4). Every family CI runs is read from this PR's check-runs, not re-run here.Generated by Claude Code