Repository navigation
feat(spec)!: an authored connector URL holds no credential part; its retryConfig and actions refuse an undeclared key - #29573
Conversation
… connector shapes close on the authored entry Claude-Session: https://claude.ai/code/session_01Q4EjjP1K353tLKWRMajLsE Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4EjjP1K353tLKWRMajLsE Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4EjjP1K353tLKWRMajLsE Co-authored-by: Claude <noreply@anthropic.com>
…ry narrowings Claude-Session: https://claude.ai/code/session_01Q4EjjP1K353tLKWRMajLsE Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4EjjP1K353tLKWRMajLsE Co-authored-by: Claude <noreply@anthropic.com>
… and on the read The write door's refinement and the read redactor now run the same traversal (findConnectorUrlCredentials); the redactor maps each URL it returns to the position it can address. Nested-key test assertions pin the issue shape instead of message wording; the dogfood pins that a row stored with a credential part in a providerConfig URL keeps dispatching with its stored URL. Claude-Session: https://claude.ai/code/session_01Q4EjjP1K353tLKWRMajLsE Co-authored-by: Claude <noreply@anthropic.com>
…ontext The save door now refuses a credential part in a connector URL, so the carry-forward pin's row is written the way such a row reaches a store today: a disabled clean save, then the system context writes the stored form. The pin then also measures that the runtime materializes a stored row as it is. The duplicate stored-row pin added beside the new door pins is dropped in favour of this one. Claude-Session: https://claude.ai/code/session_01Q4EjjP1K353tLKWRMajLsE Co-authored-by: Claude <noreply@anthropic.com>
…till open `connectors[].retryConfig` was the nested strip site the two authoring-lint pins walked; the authored connector entry closes it, so the parse refuses the key and the lint correctly stays quiet. The pins now read `apis[].objectParams`, and the connector fixture stays as a closed collection with its hand-off (a located refusal at retryConfig) asserted. The redactor's module note names `auth` beside `providerConfig` for the array and dotted-key positions, as the shared walk covers both roots. Claude-Session: https://claude.ai/code/session_01Q4EjjP1K353tLKWRMajLsE Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. 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 fafe515edaf84aeba42f3bc90a1c6aff95a138a7 && git checkout fafe515edaf84aeba42f3bc90a1c6aff95a138a7
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c76c0e042ea1efc9ccb7b209a82dd8f0fc119953 7d079947412d31180bc9427968bca044dc0b9554 && git checkout -B drift-repro c76c0e042ea1efc9ccb7b209a82dd8f0fc119953 && git merge --no-ff 7d079947412d31180bc9427968bca044dc0b9554
node scripts/docs-audit/affected-docs.mjs --json c76c0e042ea1efc9ccb7b209a82dd8f0fc119953
|
…rescribe; fixtures follow The closed retryConfig and action shapes of an authored connector entry are exported as DeclarativeConnectorEntryRetryConfigSchema and DeclarativeConnectorEntryActionSchema. They are the shapes a metadata document is parsed against, so the authorable-surface gate judges a deletion from their shared declaration on them; the open RetryConfig and ConnectorAction are reached by no metadata root. A credential-shaped key inside either meets the entry's credential prescription, and an execution binding on an action meets ADR-0097 section 5's. Forced test edits: the authorable-surface gate fixtures move to the retry twin, the CLI package-union probe moves to apis[].objectParams; the door pins gain an inline openapi document case and its control. Claude-Session: https://claude.ai/code/session_01Q4EjjP1K353tLKWRMajLsE Co-authored-by: Claude <noreply@anthropic.com>
…rate the spec artifacts ADR-0122 type aliases for DeclarativeConnectorEntryRetryConfig and DeclarativeConnectorEntryAction (with their Parsed states), and the generated artifacts check:generated proved stale: api-surface, export-origins, declaration-map, the authorable surface and defaults, the JSON Schema manifest, and the reference docs and their index. Claude-Session: https://claude.ai/code/session_01Q4EjjP1K353tLKWRMajLsE Co-authored-by: Claude <noreply@anthropic.com>
…e open shapes' field instances Exporting the closed retryConfig and action twins added public names and published defs; neither is needed. Each twin is now built from the same field instances as its open counterpart, so the authorable-surface gate reaches integration/RetryConfig and integration/ConnectorAction through its derived-clone bridge and a bare deletion from the shared declaration is still refused. The twins keep their guidance tables. The generated artifacts return to the merge base's bytes; the #17356 fixture moves to integration/DeclarativeConnectorEntry, the connector root's own def, so its eager reading stays a root-graph hit. Claude-Session: https://claude.ai/code/session_01Q4EjjP1K353tLKWRMajLsE Co-authored-by: Claude <noreply@anthropic.com>
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 577984f · 2026-11-02T23:01Z Taken under (b): the head has waited 75 min, with no lane signature, record, claim or hold. Generated by Claude Code |
|
domain:spec#1 · hotlong · session_01Q4EjjP1K353tLKWRMajLsE Contract reviewServed-tier: This record supersedes the one on Check-runs on this head at review time, all completed: success — Test Core (and shards 1/8 to 8/8), Test UI (and shards 1/16 to 16/16), TypeScript Type Check, Lint & Repo Gates, Type Check UI, Build & E2E UI, Build Core, Build UI Deps, Live E2E UI, Plan Test Core shards, Dogfood Regression Gate, Auto Label, Check Documentation Links, Lint · scoped · tooling self-tests, Type Check · consumer gates, Type Check · workspace, Lint · PM dispatch-gates, Lint · CI, release & engine gates, Type Check · source gates, Lint · scoped · query-options erasure, Type Check · debt ledger, Lint · docs & code guards, Lint · scoped · slot lookup & corpus, Lint · repo & tooling gates, Lint · ESLint, Flag docs affected by code changes, Governed Surface Queue Guard, PR-body guards, filter, Spec property liveness, Check PR Size, Check Changeset; skipped (opt-in or matrix placeholders) — Lint UI, UI Published Artifacts, Build Docs, Dogfood Regression Gate shard placeholder, Temporal Conformance, Dogfood Verify CLI, 18.x upgrade harness, Packed-tarball smoke, Schedule gate. Nothing is in progress and nothing is red. ① Derived judgmentsWhat the delta changes, each judged:
Caller classes, unchanged from the first record and still holding: read + write (served form saves and keeps stored values; an edit stores the edit; a refused body leaves the row as it was); read-only (member and admin withheld alike on item / layered / both list routes); write without read (the refusal holds on the body as sent; the carry-forward re-grafts only where the incoming value equals the served form); create-only (nothing to carry; a ② Semver levelUnchanged and still correct: two changesets, ③ Boundary flags
④ Design judgment
Findings: must-fix — none. Should-fix — S1 (open item on the shared URL module, carried to the maintainer), S3 (pin Implemented-by: VERDICT: PASS |
|
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U Contract reviewServed-tier: Inputs read:
① Derived judgments
② Semver levelThere are two changesets:
③ Boundary flags
④ Design judgmentThe design makes the ruling mechanical: one walk serves both doors, so the two cannot drift. Rewriting the read side onto the write's walk is the simplification the old twin copies lacked. Closing the nested shapes from the same shape builders keeps open and closed key sets equal by construction, and the new test pins that equality. Tried to break it:
Must-fix: none. Should-fix:
Implemented-by: VERDICT: PASS Generated by Claude Code |
|
domain:skills#2 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Takeover · 2026-11-03T02:11ZBy the maintainer's order, this PR and its landing pass to seat session
The previous holder, Readings at takeover:
Next steps, in order:
The open question in 维护者速读 (does |
|
domain:skills#2 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Ruling ·
|
…stored one whole
Ruling 6593804783: a save that sends retryConfig (the served form, {} or any
object) replaces the stored container whole; a key the closed retryConfig does
not declare is not carried forward into it, and the save names each key it
dropped. A save that leaves retryConfig out removes it, as before.
Pinned at the spec redactor, the metadata-protocol carry-forward and the HTTP
save door, with controls: a withheld header map on the same save is still
carried forward, omission still removes the container, and a non-object
retryConfig is still kept at rest by a save that leaves it out.
Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj
Co-authored-by: Claude <noreply@anthropic.com>
…t retryConfig replaces the stored one whole Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…rs a connector's undeclared retryConfig keys Regenerated content/docs/references with gen:docs (check:generated --fix). Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
…door serves them Claude-Session: https://claude.ai/code/session_01QSeKYn5CmmdShCJifm6WYj Co-authored-by: Claude <noreply@anthropic.com>
|
domain:skills#2 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj {
"issue": null,
"status": "done",
"branch": "claude/connector-metadata-hardening",
"pr": "https://github.com/objectstack-ai/objectstack/pull/29573",
"session": "session_01QSeKYn5CmmdShCJifm6WYj (subagent: the dispatching seat's session)",
"head_sha": "7d079947412d31180bc9427968bca044dc0b9554",
"premise_still_valid": true,
"summary": "Step 1: merged origin/main at 0eb97fa47 into the branch through scripts/pm/os-regen-merge.sh (real merge commit 9a0d87754, no rebase, no force-push, no conflicts). Step 2, premise read BEFORE editing: instance materialization (service-automation plugin.ts) parses the stored retryConfig with the open RetryConfigSchema (z.object, strip) and hands the provider only parsed.data; measured on src and again on the built dist, a policy carrying two undeclared keys parses successfully, the keys are absent from the parsed policy, and the parsed policy and connectorFetchOptions output are identical with and without them. One raw read exists and is recorded, not hidden: the reconcile fingerprint connectorInstanceSignature serializes the stored retryConfig as written, so the save that first drops such a key changes the fingerprint and the next reconcile re-materializes the instance once with the identical policy; no retry or dispatch behaviour reads the key, so the premise holds as the ruling states it. Ruling implemented at the carry-forward decision point the protocol already reads: the connector redactor now names the undeclared keys of an object retryConfig in notCarriedForwardKeys (main's contract for undeclared auth keys), so an explicit retryConfig (the served form, {}, any object) replaces the stored one whole and the save answer names each dropped key (advisory withheld-key-dropped); omission still removes the container; an actions element's key and a non-object retryConfig stay carried forward; no metadata-protocol source changed. Pinned at the spec redactor, the metadata-protocol carry-forward and the HTTP door (dogfood) with controls, ablated once. Changeset, D3 upgrade entry, the advisories .describe() (+ regenerated protocol.mdx) and the PR body (one relay issue_patch: 维护者速读 需要你做的 now says the ruling is implemented) updated at rule level.",
"tests": "All under scripts/pm/os-verify-lock.sh, exit read from its VERDICT line, on head 7d0799474: pnpm --filter @objectstack/spec test → exit 0, 805 files, 23864 passed, 1 todo; pnpm --filter @objectstack/metadata-protocol test → exit 0, 308 files passed 4 skipped, 22272 passed 26 skipped; the three connector-door dogfood files → exit 0, 44/44 (url-and-nested-keys now 26 cases); spec / metadata-protocol / dogfood typecheck → exit 0 x3; spec repo project scripts/build-schemas-check-mode.deleted-baseline.test.ts → exit 0, 15/15; cli integration test/package-union-readers.test.ts → exit 0, 7/7. First dogfood run of the new pin was red on my own envelope guess (data.advisories): the meta save door serves the result unwrapped; fixed in 7d0799474 and re-run green. Ablation (one run, implementation committed first, own script with trap EXIT INT TERM, absolute paths): anchor `notCarriedForwardKeys.push(path.join('.'))` replaced via scripts/ablation-replace.mjs so retryConfig keys stay withheld but carried forward (anchor-count 0, marker-count 1, blob 14a0ea3f vs HEAD fa790442); spec rebuilt; ablation-dist-preflight marker present in 4 built files. Mutated: spec redaction file 3 red / 64 green, metadata-protocol file 5 red / 11 green, dogfood 3 files 3 red / 41 green (both CONTROLs green) - every red one predicted before the run. Restored: blob == HEAD blob, git diff HEAD empty, whole-tree git status --porcelain empty; spec rebuilt; preflight --absent: marker absent from all 592 built files; re-run 67/67, 16/16, 44/44.",
"gates": "dispatch-gates --commands --repo objectstack-ai/objectstack on 7d0799474 (20 paths vs merge base 0eb97fa47) → 108 commands; each run with output redirected and exit captured before any pipe; --ran on the `command :: exit N` record → 108 derived, 108 run, 0 NOT-MEASURED (derived from recorded codes), 0 UNRUN, all exit 0. check:dual-build-cjs-loads first exit 3 (PREREQUISITE NOT MET: 7 packages outside the dogfood closure unbuilt), built them, re-run exit 0. check:type-check-debt exit 0 (nested in a locked run). Named by the order: check:authorable-surface exit 0; check:strictness-ledger exit 0 (counts current, 14 shards); spec check:generated 14/14 up to date (after check:generated --fix regenerated content/docs/references/api/protocol.mdx for the .describe() edit); check:api-surface unchanged; check:nul-bytes exit 0. CI on 7d0799474 (read, not awaited): 65 check-runs, 57 success, 8 skipped, 0 failed; required contexts Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Type Check UI, Test UI, Build & E2E UI, Live E2E UI success, Lint UI skipped; Check Changeset success. PR-body guards re-run on the body edit is CI's reading.",
"dogfood_battery": "7d0799474 · os-verify-lock VERDICT command-exit 0 (held 1447s) · pnpm --filter @objectstack/dogfood test, no shard, no path · Test Files 402 passed, 3 skipped (405) · Tests 3336 passed, 28 skipped (3364) · no failures",
"line_budget": "n/a - no skills/** file touched; PR net diff vs main 20 files +1603/-205 (under 3000); this round 9 files +197/-48 plus the merge",
"files_changed": [
"packages/spec/src/integration/connector-credential-redaction.ts (the ruling: undeclaredRetryConfigKeysOf → notCarriedForwardKeys; module note item 8)",
"packages/spec/src/integration/connector-credential-redaction.test.ts",
"packages/metadata-protocol/src/metadata-redaction.connector-undeclared-keys.test.ts (test only; main's file, forced by the ruling)",
"packages/qa/dogfood/test/connector-meta-door-url-and-nested-keys.dogfood.test.ts (the {} pin + controls)",
"packages/qa/dogfood/test/connector-meta-door-undeclared-keys-withheld.dogfood.test.ts (forced flip)",
"packages/spec/src/api/protocol.zod.ts (advisories .describe())",
"content/docs/references/api/protocol.mdx (generated by gen:docs)",
".changeset/connector-entry-nested-undeclared-key-refused.md",
"packages/spec/src/migrations/entries/semantic/18.connector-entry-nested-undeclared-key-refused.ts (reason text)"
],
"merge_conflicts": "none. Merge 9a0d87754 (origin/main 0eb97fa47, pre-merge base 73c24394). Auto-merged because both sides moved them: packages/spec/src/integration/connector-credential-redaction.ts and its .test.ts - main added notCarriedForwardKeys for undeclared auth keys (#29456), the branch moved the URL walk to connector-url-credentials.ts; both sides' hunks are in the merged file, read and tested. os-regen: docs/audits/2026-07-unknown-key-strictness-ledger.counts/integration.md changed by the branch only, the script kept its bytes; check:strictness-ledger and check:generated read every generated artifact current after the merge, so no regeneration was owed by the merge itself.",
"deviations": [
"Conflict with the dispatch wording, named not resolved silently: the control was written as `omitting retryConfig still carries forward`, but per the code and the PR's own earlier pin an omitted retryConfig is removed with its keys, before and after this change. Pinned two controls instead: omission removes as before (the ruling's `keeps today's behaviour`), and a non-object retryConfig (withheld whole) IS still carried forward by a save that leaves it out - the one case where omission carries forward.",
"Consequence of reusing notCarriedForwardKeys, declared in the PR body: a save that omits retryConfig stores the same as before but its answer now also names each dropped undeclared retry key (withheld-key-dropped), as an omitted auth already does.",
"Beyond the literal ask, to keep published text true: SaveMetaItemResponseSchema.advisories .describe() named only undeclared auth keys - widened and protocol.mdx regenerated; the D3 entry's reason said a served-form save keeps the key - corrected.",
"PR body opening line re-signed to the current seat (domain:skills#2 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj) after the takeover; Seat notes rewritten (takeover, ruling implemented, contract review owed on the new head).",
"Commit trailers are AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude), not the harness reminder's model-named trailer.",
"Stray file created by a shell-scoping mistake: /build2.pid at the filesystem root (16 bytes, BUILD_PID=11485). Its removal was blocked by the safety check and not worked around; a human should run rm /build2.pid.",
"Lock: two queue-timeouts (exit 99) re-run with kept slots; no run went unlocked.",
"First post-stamped --body attempt was refused locally (two newer comments unacknowledged), nothing written; the second with --ack-through=6593804783 wrote; one PATCH landed.",
"origin/main (shared ref) advanced to c76c0e042 during the round; not re-merged (AGENTS §10: only on a reported conflict, a main-caused red or a queue removal)."
],
"mcp_calls": "0",
"api_writes": "2 relay strokes (each one repository_dispatch from this session, executed as objectstack-fleet[bot]): issue_patch objectstack-ai/objectstack#29573 (PR body, read back 25661 bytes identical, run 41578107110); comment on #29573 (this report). git push x5 to the branch (not REST). No labels, no reviewers, draft untouched.",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: none · noted, not filed: comments naming the notCarriedForwardKeys class by its auth example only (spec kernel metadata-type-redaction.ts TSDoc, metadata-protocol metadata-redaction.ts docblocks) now under-describe it by one case; examples, not claims of completeness - polish, left as is.",
"carrier: none · noted, not filed: connectorInstanceSignature hashes the raw stored retryConfig, so an undeclared retry key participates in change detection although nothing reads it; with the closed shape and this ruling such keys only drain, so the cost is one extra re-materialization per affected row - no defect."
]
} |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract-review-claim: session_01QSeKYn5CmmdShCJifm6WYj · head 7d07994 · 2026-11-03T04:30Z |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj Contract reviewServed-tier: Full review of the whole PR at this head, not a delta: the head moved from Check-runs on this head: 69, all ① Derived judgmentsEvery accept-set and public-surface change, judged in the tree:
Caller classes, each read against the doors: read + write (served form saves; stored URLs and ② Semver levelTwo changesets, both ③ Boundary flagsThe earlier records' items, the dev's deviations and the Acceptance notes, each answered:
④ Design judgment
Findings: must-fix — none. Should-fix — S1 (the shared predicate's scope, with the maintainer, at rule level), S4 (a depth specimen beneath the unregistered root, low). Delivered since the PASS records — S3 (the Implemented-by: VERDICT: PASS |
|
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj 席位核对ACCEPT · head
|
domain:skills#2 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj
Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — every connector entry that
skills/**, the hand-writtencontent/docs/**,examples/**andpackages/apps/**author (eachdefineConnector({…})andconnectors: [{…}]literal, scanned) already keeps its URLs credential-free and declares onlyRetryConfigSchema/ConnectorActionSchemakeys insideretryConfigandactions, so nothing they teach changes; each refusal carries its own prescription.维护者速读
providerConfig与auth下任何层级的 URL 若带 userinfo 或凭据类查询参数,在 defineStack、os validate/compile、构建产物加载与保存门处被拒(保存为 422INVALID_METADATA,逐个 URL 指出位置,提示改用auth.credentialRef);条目里的retryConfig与每个actions元素改为封闭结构,未声明的键按名拒绝(凭据类键给出auth.credentialRef的改法,action 上的method/path/url给出 ADR-0097 §5 的说明)(两个封闭结构仍为模块私有、不导出、不发布,与开放结构共用同一组字段实例);连接器读取对auth端点套用与providerConfig同一遍历、同一判据的凭据部分改写。另按维护者裁决:保存时显式给出的retryConfig({}、读取所得原样或任何对象)整体替换库中的retryConfig,库中行的未声明重试键不再沿用,保存应答逐个点名被丢弃的键;省略retryConfig照旧移除整个容器。retryConfig的未声明键没有任何写入门接受、运行时也不读取(物化时用开放的RetryConfigSchema解析并丢弃),沿用它只会把库中明文留下。retryConfig/actions里写了未声明键的代码与包,升级后在校验、构建产物加载与保存时被拒;库中已存的行不重判、照常运行,读取隐去凭据部分,原样保存保留库值(retryConfig的未声明键除外:首次显式保存时被丢弃并在应答中点名;库中字节变了,该实例在下一次对账时按完全相同的策略重新物化一次);不做存量迁移。回滚即 revert 本 PR。packages/spec/src/integration/connector.zod.ts(refinement 中逐 URL 拒绝;sharedFields让封闭嵌套结构与开放结构共用字段实例,及其 guidance)、packages/spec/src/integration/connector-credential-redaction.ts(读取隐去的位置;undeclaredRetryConfigKeysOf把未声明重试键列入notCarriedForwardKeys,即本轮裁决的落点)、packages/spec/src/integration/connector-url-credentials.ts(唯一遍历findConnectorUrlCredentials与读取位置映射)。pnpm --filter @objectstack/dogfood exec vitest run test/connector-meta-door-url-and-nested-keys.dogfood.test.ts,预期 26 个用例全部通过。retryConfig: {}的裁决已在本 PR 实现,见 PR 评论 6593804783)。Seat notes
Refreshed 2026-11-03T04:26Z.
session_01QSeKYn5CmmdShCJifm6WYj(comment 6593633433); this body is maintained by that seat from here on.retryConfig: {}over a stored row clear the keys the read withholds) was ruled in comment 6593804783: an explicitretryConfigreplaces the stored one whole. Its named premise was verified before the edit and holds (Acceptance notes); the ruling and the pin the earlier should-fix named are on this head.577984fe21163e30c191ccced287a3e1a0ba9e31. The merge oforigin/mainand the ruling's implementation owe a contract-review-tier review on the new head before landing. The remaining should-fix from those records (a depth specimen for the deleted-baseline pin) is unchanged; the items on the shared URL predicate stay with the maintainer, at rule level only here.session_01Q4EjjP1K353tLKWRMajLsE; by design there is no public card for it.landing-operations.md§D), per the takeover record.What changes
BREAKING — two accept-set narrowings on an authored connector entry (
DeclarativeConnectorEntrySchema), each with its changeset and its ADR-0087 D3 upgrade-guide entry.1. An authored connector URL holds no credential part. At every door that runs the entry's refinement —
defineStack({ connectors }),os validate/os compile, the metadata plugin's artifact load at boot and on reload, andPUT /api/v1/meta/connector/:name— a URL under an instance'sproviderConfig(any key, any depth, array elements included) orauth(tokenUrl,authorizationUrl,resource) holding a userinfo or a credential query parameter with a value is refused: one issue per URL, located at the URL, naming the parts it holds (never the URL or its value) and prescribingauth: { type, credentialRef }. At the save door that is422 INVALID_METADATA, and nothing is stored. There is no rule per provider: a URL is recognised by its value.One walk, both doors.
connector-url-credentials.tsholds the one definition of a credential part and the one traversal over an entry's URLs (findConnectorUrlCredentials). The write refinement refuses every URL it returns; the read redactor maps each to the position it can address (connectorUrlCredentialPositionOf: rewritten in place, or the enclosing array or dotted key withheld whole) — so the read rewrites or withholds exactly the URLs the write refuses, and the read now covers theauthendpoints as it coversproviderConfig. A URL inside a header or environment map (providerConfig.headers,defaultHeaders,transport.headers,transport.env) is judged by neither: the read withholds those maps whole.2. An authored connector entry's nested shapes are closed.
retryConfigand eachactionselement refuse a key they do not declare, in oneunrecognized_keysissue located at the container (retryConfig,actions.N), at every door that parses the entry,defineConnector()included. A credential-shaped key (secret,token,apiKey,password,credentialRef, …) carries the entry's own prescription (auth.credentialRef); an execution binding on an action (method,path,url) carries ADR-0097 §5's (an action carries none). The open shapes (ConnectorSchema,RetryConfigSchema,ConnectorActionSchema) keep their posture, because the runtime parses a stored row's retry policy with the openRetryConfigSchemaat materialization and a provider derives actions through the open action shape.Twins stay private, sharing field instances. The closed nested shapes are module-private and emit no def; nothing is added to the published surface. Each is built from the SAME field instances as its open counterpart (
sharedFields), because the authorable-surface deletion gate judges a def's reachability by instance: with per-carrier instances the openintegration/RetryConfigandintegration/ConnectorActionwere reached by no metadata root, so a bare deletion from the shared declaration would have been waived as unreachable — the build-schemas fixture failure on the first push of this PR was that. With shared instances the gate's derived-clone bridge reaches both open defs through the twins. The guidance census then pairs the open defs with the twins'guidance; that pairing is consulted only for a deleted baseline key the guidance names, and every key it names is one the open defs never declared.Unchanged. The runtime materializes a stored row as it is stored and never re-judges it by the entry's refinement, so a connector stored before these refusals keeps dispatching; a save of its served form keeps the stored values (the metadata protocol's carry-forward) except a stored undeclared
retryConfigkey (3. below), and an edit of a served URL stores exactly the edit. No stored-row migration is shipped.3. An explicit
retryConfigin a save replaces the stored one whole (the maintainer's ruling, comment 6593804783). A save that sendsretryConfig— the served form,{}, any object — does not carry a stored row's undeclared retry key forward, and its answer names each key it dropped (advisory rulewithheld-key-dropped, pathconnectors[0].retryConfig.secretfor a keysecret). A save that leavesretryConfigout removes it, as before. An undeclared key in anactionselement, aretryConfigstored as a non-object (withheld whole), and every other withheld position are carried forward as before. The mechanism is declarative: the connector read redactor names the undeclared keys of an objectretryConfiginnotCarriedForwardKeys— the contract the undeclaredauthkeys already use — and the metadata protocol's carry-forward reads it, so no protocol code changes. Theadvisoriesdescription on the save answer (SaveMetaItemResponseSchema) now names this case, andcontent/docs/references/api/protocol.mdxis regenerated from it.FROM → TO
Set the secret under the name
credentialRefgives (an environment variable in the open tier, or Setup → Connector credentials). Upgrade-guide entries:connector-entry-url-credential-refused,connector-entry-nested-undeclared-key-refused.Why
Connector metadata holds no credential at any position, and the save door stores the body it accepts (ADR-0005 appendix (c), ruling B). The maintainer's ruling for this change, verbatim:
and, for the nested shapes, to close them unless an ADR deliberately keeps them open. None does: ADR-0097, ADR-0143, ADR-0023, ADR-0024, ADR-0104 and the strictness ledger's
integration/row were read, and ADR-0097 §5 states that an action carries no execution binding. So they close with the entry, declared as breaking.For a save over a stored row, the maintainer answered the question this PR left open (comment 6593804783) with the option put first, verbatim:
recorded there in three parts: an explicit
retryConfig(including{}) replaces the stored container whole and the stored row's withheld undeclared keys are not carried forward into it, omittingretryConfigkeeps today's behaviour; the premise that the runtime reads no undeclaredretryConfigkey when it materializes a stored row is verified before the edit; and if it does not hold, the work stops rather than forcing the clear or keeping the old behaviour.Tests
src/integration/connector-entry-url-credentials.test.tsConnectorSchemaunchanged; the write door refuses exactly what the read rewrites, and at every depth the read withholds each refused URL at its addressable positionsrc/integration/connector-entry-nested-keys.test.tsretryConfigandactionselements refuse undeclared keys bycode+path+keys; a credential-shaped key in either carries the credential prescription and an action'smethod/path/urlcarries ADR-0097 §5's (prescription text pinned); an action's ownkeystays its machine name;defineConnector()refuses through aZodErrorwith the same issue; the open carriers drop, not refuse; the private twins declare exactly the open shapes' keys, as the same field instancessrc/integration/connector-credential-redaction.test.tsauth.tokenUrl/auth.authorizationUrland a credential parameter out ofauth.resource, byte for byte otherwise; the undeclared keys of an objectretryConfig, a cleared one included, are named innotCarriedForwardKeys; CONTROL: a non-objectretryConfigand anactionselement's key stay carried forwardsrc/metadata-redaction.connector-undeclared-keys.test.tsretryConfig: {}and an omittedretryConfigeach leave no undeclared retry key and report the drop; the top-level andactionskeys are still carried; the runtime authoring gate is never told the key will be restoredtest/connector-meta-door-url-and-nested-keys.dogfood.test.ts(26)422envelope at each position, a URL inside an inlineproviderConfig.specdocument included (located atproviderConfig.spec.servers.0.url), nothing stored, an existing row untouched; CONTROL saves — an inline document with credential-free URLs stored as written, instances that materialize and dispatch; member and admin item / layered / list reads of a storedauthendpoint; carry-forward of served forms and of an author's edit;retryConfig: {}over a stored row holding an undeclared retry key —200, the answer'sadvisoriesnameconnectors[0].retryConfig.secretand never its value, and the next read of the stored row holdsretryConfig: {}, while the withheld header map on the same row stays at rest (CONTROL); the served form replaces too; CONTROL: leavingretryConfigout removes it as before, and a non-objectretryConfigis still kept at rest by a save that leaves it outForced test edits (fixtures the narrowing makes refusable at the save door, now seeded the way such a row reaches a store, through the system context):
connector-meta-door-undeclared-keys-withheld.dogfood.test.ts(the nestedretryConfig/actionsrows),connector-meta-door-entry-and-url-hardening.dogfood.test.ts(the carry-forward row; saved disabled first, so the pin now also measures that the runtime materializes the stored row as it is),packages/spec/src/kernel/metadata-authoring-lint.test.ts(its two pins walkedconnectors[].retryConfigas the remaining nested strip site; they now walkapis[].objectParams, and the connector fixture stays as a closed collection with its located refusal asserted),packages/cli/test/package-union-readers.test.ts(the package-union probe moves fromconnectors[].retryConfigtoapis[].objectParams, assertions unchanged in kind), andpackages/spec/scripts/build-schemas-check-mode.deleted-baseline.test.ts(the #17356 unregistered-kind-root fixture moves fromintegration/RetryConfig, now reached through the twin's derived-clone bridge rather than by identity, tointegration/DeclarativeConnectorEntry, the connector root's own def, so its eager reading stays a root-graph hit). Forced by the ruling (pins that asserted the old keep-on-save reading of an undeclared retry key): the served-formretryConfigtest inconnector-meta-door-undeclared-keys-withheld.dogfood.test.tsnow asserts the replacement;metadata-redaction.connector-undeclared-keys.test.tsexpects the served-form save without the retry key and its no-drop CONTROL uses a row without one; the redactor's carried-forward CONTROL inconnector-credential-redaction.test.tsno longer holds a retry key.Acceptance notes
user:password@and thetoken@form several HTTP APIs read as a token — not only a password, because the read already withholds the whole userinfo and both doors share one definition. Every built-in provider reaches its upstream throughfetch, which refuses any URL carrying a userinfo, so no configuration that could dispatch is refused by the wider reading.providerConfig.specgiven as an inline document object is walked like any other value underproviderConfig(the read already rewrote URLs inside one); a document whose example URL carries a credential-named query parameter with a value is refused. Packaging the document as a file path is unaffected.@objectstack/lintruleconnector-credential-literalwarns on a credential-shaped literal inside one.defineConnector()closes the nested shapes but does not run the entry refinement (as for the other ADR-0097 refinements); the URL refusal meets the entry atdefineStack, the CLI, the artifact load and the save door.retryConfigkey. Instance materialization (service-automationplugin.ts) parses the stored policy with the openRetryConfigSchemaand hands the provider only the parsed result; measured onsrcand again on the builtdistthe runtime loads, a policy carrying two undeclared keys parses successfully, the keys are absent from the parsed policy, and the parsed policy and the resolved fetch options (connectorFetchOptions, the one execution site) are identical with and without them. One raw read exists: the reconcile's change fingerprint (connectorInstanceSignature) serializes the storedretryConfigas written, so the save that first drops such a key changes the fingerprint and the next reconcile re-materializes the instance once, with the identical policy. No retry or dispatch behaviour reads the key.retryConfignow names the drop. What a save that leavesretryConfigout stores is unchanged (the container is removed, withheld keys included); its answer now also carries onewithheld-key-droppedadvisory per undeclared key the stored container held, as an omittedauthalready does for its undeclared keys.origin/main. Merged at0eb97fa47(merge commit9a0d87754, a real merge, throughscripts/pm/os-regen-merge.sh), no conflicts. Two files were auto-merged because both sides moved them —connector-credential-redaction.tsand its test, wheremainhad addednotCarriedForwardKeysfor undeclaredauthkeys (feat(spec): a save of a connector's served form drops a leftover undeclared auth key and names it #29456); both sides' hunks are in the merged file and that contract is what the ruling now uses. The onemerge=os-regenpath the branch changes, the strictness counts shardintegration.md, was changed by the branch only, so the script kept its bytes;check:strictness-ledgerreads it current after the merge.integration/counts regenerated (4 → 6 sites: the two closed twins).Verification
All local runs went through
scripts/pm/os-verify-lock.sh(itsVERDICT command-exitline is the reading), each exit code captured before any pipe. Final head:7d0799474(branch merged withorigin/mainat0eb97fa47). The readings below are this round's, on that head unless a row says otherwise; the previous round's readings on577984feare superseded by them. CI on this head is its own reading.Suites and typechecks
pnpm --filter @objectstack/spec test(thelocalproject)7d0799474pnpm --filter @objectstack/metadata-protocol test7d0799474url-and-nested-keys,undeclared-keys-withheld,entry-and-url-hardening)7d0799474pnpm --filter @objectstack/spec typecheck·@objectstack/metadata-protocol typecheck·@objectstack/dogfood typecheck7d0799474pnpm --filter @objectstack/spec exec vitest run --project repo scripts/build-schemas-check-mode.deleted-baseline.test.ts7d0799474pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=1 test/package-union-readers.test.ts7d0799474pnpm --filter @objectstack/dogfood test(whole battery, no shard, no path)7d0799474The rest of the spec
repoproject and the cli suites are CI's reading on this head (Test Core): of their files, this PR's diff edits only the two run above.Ablation (one run, the ruling's implementation committed first)
One mutation through
scripts/ablation-replace.mjs(anchor hit once, blob changed): the undeclaredretryConfigkeys stay withheld but are no longer named innotCarriedForwardKeys— the pre-ruling behaviour.packages/specrebuilt (tsup),scripts/ablation-dist-preflight.mjs→ the marker present in 4 built files, so the protocol and dogfood suites (which resolve@objectstack/specthroughdist/) read the mutation.connector-credential-redaction.test.ts— the threeretryConfignot-carried-forward casesretryConfigstays carried forward; every other casemetadata-redaction.connector-undeclared-keys.test.ts— served form,{}, omission each report the drop; the gate is not told; the served-form save without the keyretryConfig: {}replaces whole (with its header-map CONTROL), the served form replaces, the second file's served-formretryConfigretryConfigkept at rest; every other pin in the three filesRestore:
ablation-replace --restore;git hash-objectequalsgit rev-parse HEAD:PATH;git diff HEADempty and whole-treegit status --porcelainempty.packages/specrebuilt, thenablation-dist-preflight … --absent→ the marker absent from all 592 built files.Gates
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandson7d0799474(20 paths against merge base0eb97fa47) derived 108 commands; each ran with its exit recorded before any pipe, and--ranon thecommand :: exit Nrecord → 108 derived, 108 run, 0 NOT-MEASURED (derived from the recorded codes), 0 UNRUN; all exit 0.pnpm check:dual-build-cjs-loadsfirst answered exit 3 (PREREQUISITE NOT MET: seven packages outside the dogfood build closure had nodist/); those seven were built and it was re-run: exit 0.pnpm --filter @objectstack/spec check:generated: all 14 generated artifacts up to date;content/docs/references/api/protocol.mdxwas regenerated bycheck:generated --fix(gen:docs) for theadvisoriesdescription edit and is the only generated file this round changes;check:api-surfacereads the public API surface unchanged,check:authorable-surfaceexit 0,check:strictness-ledgercurrent (14 shards).Human Minimum
packages/specholds no runtime logic.pnpm lintpasses. (No formatter ran;pnpm lintis CI's run.)main, one pull request (a no-card order: no issue to reference).packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.