Skip to content

fix(spec)!: the datasource read redactor covers the declared ssl secret members - #28674

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-xvyjqe
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/project-thread-xvyjqe

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #28633

Clause-②: no (narrowing)
Metadata-protocol: none
Skills-docs: none — no page under skills/** or content/docs states what a metadata read serves of a datasource's ssl block; the TLS bullet in content/docs/data-modeling/drivers.mdx stays true as written.

BREAKING read-answer narrowing (@objectstack/spec, shipped as patch): the metadata read exits no longer serve a datasource's ssl.key. No authored value is refused and no stored row is rewritten.

维护者速读

  • 改了什么:数据源的元数据读出口现在连同 ssl 块里声明的密钥成员(ssl.key)一起脱敏,做法与 config 里的凭据一致:不下发,并在 redactedKeys 里点名 ssl.key。
  • 为什么改:DatasourceSchema 的 ssl.key 是客户端私钥,让它和 config 凭据走同一条声明式脱敏规则。
  • 风险与回滚:低。只让读出口少下发一个字段;存储行不改写,保存时原样带回已存的 key;ca/cert/enabled/rejectUnauthorized 照常下发。回滚即撤掉 schema 上的 credential 标记与脱敏器里那一段。
  • 先看:packages/spec/src/kernel/metadata-type-redaction.ts(redactDatasourceItem 新增的标记字段一段);packages/spec/src/data/datasource.zod.ts(ssl.key 的 credential 标记);packages/spec/src/kernel/metadata-credential-fields.test.ts(守卫多读两个词)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/kernel/metadata-type-redaction.test.ts,预期全部通过(main 上新增的 4 条为红)。
  • 需要你做的:无,席位落地。

What changes

The datasource read redactor covers the declared ssl secret members.

  • packages/spec/src/data/datasource.zod.ts: DatasourceSchema.ssl.key (the client private key) is marked .meta({ credential: 'value' }), the schema-declared credential marker from ruling B+ on meta: the webhook metadata type has no redactor, so its secret-bearing fields are not masked on the meta read doors (detail withheld under the disclosure ruling) #24966 (kernel/metadata-credential-fields.ts). It is the only secret member of the declared ssl block (enabled, rejectUnauthorized, ca, cert, key); ca and cert are public material and are not marked. The block declares no passphrase.
  • packages/spec/src/kernel/metadata-type-redaction.ts: redactDatasourceItem keeps its config half (redactDatasourceConfig, unchanged) and adds a second half that applies the credential markers on the registered datasource schema, through the same derivation the third resolution uses (derivedCredentialRedactorFor). The built-in wins the lookup over the derivation, so without this half a marker on DatasourceSchema was never read. The result names ssl.key in redactedKeys in the same dotted shape as config.password, sorted together with the config paths. An item with nothing to withhold is still returned by reference.
  • packages/spec/src/kernel/metadata-credential-fields.test.ts: the credential-field ratchet's description rule also reads private key and passphrase. With it, an unmarked ssl.key reds the ratchet (datasource:ssl.key), so the marker cannot be dropped silently.
  • Tests (below) and .changeset/28633-datasource-ssl-key-redaction.md (@objectstack/spec patch).

Why the marker, not a second hand list. The order prefers deriving the set from schema credential markers where the module supports it, and it does: the marker is the one declaration, the derivation already exists, and the ratchet beside it now catches a forgotten one. The config rule in data/datasource-credential-redaction.ts stays the home of driver-config credentials (derived from each driver's z.never() contract); ssl is not a driver-config member, so it is not added there.

Consumers inherit it. Every read exit resolves the datasource redactor through getMetadataTypeRedactor, so none needed a code change. Measured and pinned:

Read exit Resolves through Pinned in
/meta list, item, layered (getMetaItems, getMetaItem, getMetaItemLayered) redactMetadataItem → registry packages/metadata-protocol/src/protocol.metadata-redaction.test.ts
sys_metadata / sys_metadata_history through the data API (findData, getData) metadata-protocol redactStoredMetadataRow → registry packages/metadata-protocol/src/protocol.data-door-stored-metadata-redaction.test.ts
a code-registered datasource served by the metadata service (getPublished) MetadataManager.redactServedBody → registry packages/metadata/src/metadata-service.test.ts
the spec stored-row and stored-body seams (redactStoredMetadataRow / redactStoredMetadataBody: the MCP stdio data bridge, the audit writer, the automation CRUD nodes) registry packages/spec/src/kernel/metadata-type-redaction.test.ts

Write path. The generic carry-forward (carryForwardRedactedValues) walks redactedKeys, so a save that sends back the served body keeps the stored ssl.key; pinned by a GET, edit, PUT round trip.

Tests

New and extended cases, all in existing files:

  • packages/spec/src/kernel/metadata-type-redaction.test.ts, new block (6 cases): ssl.key dropped and named while enabled / rejectUnauthorized / ca / cert stay; withheld with no config object at all; config.password and ssl.key together in one sorted redactedKeys; the stored-row seam on a serialized body. Controls: no ssl block, and an ssl block with no key, are returned by reference with redactedKeys: []; the cleared form (key: '') is served as written. The existing config cases are unchanged and green.
  • packages/metadata-protocol/src/protocol.metadata-redaction.test.ts (3 cases): getMetaItems, getMetaItem, getMetaItemLayered (overlay and effective) serve ssl without key and the stored row keeps it; control with no key; GET, edit, PUT keeps the stored key.
  • packages/metadata-protocol/src/protocol.data-door-stored-metadata-redaction.test.ts (3 cases): findData and getData on sys_metadata, a sys_metadata_history snapshot; control body served as its stored bytes.
  • packages/metadata/src/metadata-service.test.ts (1 case): a code-registered datasource through getPublished, with the real built-in redactor.
Red first, green, ablation
  • Red first (origin/main sources, new spec cases in place): src/kernel/metadata-type-redaction.test.ts gave Tests 4 failed | 35 passed (39); the four red cases are the withholding cases, the controls passed.
  • Green at head: the same file and metadata-credential-fields.test.ts plus src/data/: Test Files 155 passed (155), Tests 4748 passed | 1 todo.
  • Ablation with node scripts/ablation-replace.mjs (each restore blob == HEAD, git diff HEAD empty):
    • A1, the marked-fields half of redactDatasourceItem removed: Tests 4 failed | 44 passed (48) over the redaction and ratchet files; the four withholding cases red, controls green.
    • A2, the credential marker removed from ssl.key: Tests 5 failed | 43 passed (48): the four withholding cases and the ratchet (expected [ 'datasource:ssl.key' ] to deeply equal []).
    • A3, the marker removed with the ratchet's description rule held at its origin/main text: ratchet Tests 9 passed (9), which is why the rule gains private key / passphrase.
    • A4, consumers through the built dist/: the A1 mutation spelled (globalThis as any).ABLATION_28633, pnpm --filter @objectstack/spec build, ablation-dist-preflight.mjs (dist reading passed), then metadata-protocol Tests 4 failed | 66 passed (70) (every new withholding case) and metadata 1 failed (the new case). Restore leg rebuilt spec; ablation-dist-preflight.mjs ... --absent exit 0, dist and tree both clean.

Acceptance notes

Verification

All readings on head a2d8e887 (two commits on origin/main 6b9c1ca9; the diff is the eight files above).

  • Consumer suites (pnpm --filter PKG test, under the lock, after building their closures): @objectstack/spec 782 files / 23128 tests passed; @objectstack/metadata-protocol 291 passed, 4 skipped / 21950 passed; @objectstack/metadata 65 / 919; @objectstack/rest 363 / 6910 (332 skipped); @objectstack/runtime 503 / 8171 (19 skipped); @objectstack/service-datasource 67 / 1125; @objectstack/mcp 48 / 612; @objectstack/plugin-audit 68 / 1109; @objectstack/service-automation 251 / 3185. All exit 0. These are every package outside spec that imports getMetadataTypeRedactor or a stored-body seam (redactStoredMetadataBody, redactStoredMetadataRow(s), redactMetadataItem).
  • Typecheck: pnpm --filter @objectstack/spec typecheck exit 0; @objectstack/metadata-protocol + @objectstack/metadata typecheck exit 0.
  • Derived gate families: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack → 80 families, each run with its exit captured before any pipe and stdin from /dev/null: 80 × exit 0. Two needed a prerequisite first: check-plugin-teardown-shape.mjs --self-test answered exit 3 on the shallow clone and exit 0 after git fetch --depth=1 of its fixture commit; check:dual-build-cjs-loads answered PREREQUISITE NOT MET (exit 3) before a full build and exit 0 after pnpm build --concurrency=2 (115 entry points across 70 packages load). --ran: ✓ dispatch-gates --ran: 80 derived famil(ies) accounted for — 80 run, 0 NOT-MEASURED. No generated artifact moved (check:docs, check:authorable-surface, check:api-surface exit 0, tree clean).
  • Hand gates: node scripts/check-commit-card-trailers.mjs --range origin/main..HEAD exit 0; node scripts/check-platform-checklist.mjs --changed-from origin/main exit 0 (1 anchored file, 2 anchors judged, 0 lost); pnpm exec eslint --no-inline-config on the changed .ts files exit 0; control-byte scan of the changed files: no hits. The repo-wide pnpm lint is CI's.
  • Full dogfood (owed by the (narrowing) arm), after the full build, under the lock, pnpm --filter @objectstack/dogfood test → Test Files 369 passed | 3 skipped (372), Tests 2981 passed | 28 skipped (3009), VERDICT command-exit 0.
  • CI families (Test Core incl. test:repo, the lint farm, the type-check lanes) are read off this PR's check-runs by the seat.

Generated by Claude Code

…t members

DatasourceSchema marks ssl.key as a credential value, and the built-in
datasource redactor applies the schema's credential markers beside its
config rule, so every metadata read exit names ssl.key in redactedKeys
and serves the public ssl members unchanged. The credential-field ratchet
also reads 'private key' and 'passphrase' in a field description.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pQZ6YVyVMRMk27xRe
The /meta item, list and layered reads, the sys_metadata data door and the
metadata service's served body withhold ssl.key, keep the public ssl members,
and a GET-edit-PUT round trip keeps the stored key.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FEkQ5pQZ6YVyVMRMk27xRe
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 2 documentable anchor(s).

⛔ 2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v17/17-0.mdx (via DatasourceSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-6.mdx (via DatasourceSchema (symbol, a top-level const))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 168 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 2678600d57f70f6913384f03f65c12e480781486 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from f85395814eda2185bcd090ace6e633d091ba6d1e — the merge of head a2d8e887a81615b456977522e92d664f280bc80d into base 2678600d57f70f6913384f03f65c12e480781486, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin f85395814eda2185bcd090ace6e633d091ba6d1e && git checkout f85395814eda2185bcd090ace6e633d091ba6d1e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2678600d57f70f6913384f03f65c12e480781486 a2d8e887a81615b456977522e92d664f280bc80d && git checkout -B drift-repro 2678600d57f70f6913384f03f65c12e480781486 && git merge --no-ff a2d8e887a81615b456977522e92d664f280bc80d

node scripts/docs-audit/affected-docs.mjs --json 2678600d57f70f6913384f03f65c12e480781486

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 2678600d57f70f6913384f03f65c12e480781486 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: a2d8e887a81615b456977522e92d664f280bc80d
Local-runs: none
Scope: packages/spec/src/data/datasource.zod.ts, packages/spec/src/kernel/metadata-type-redaction.ts, packages/spec/src/kernel/metadata-type-redaction.test.ts, packages/spec/src/kernel/metadata-credential-fields.test.ts, packages/metadata-protocol/src/protocol.metadata-redaction.test.ts, packages/metadata-protocol/src/protocol.data-door-stored-metadata-redaction.test.ts, packages/metadata/src/metadata-service.test.ts + packages/spec/src/kernel/metadata-credential-fields.ts, packages/spec/src/kernel/metadata-type-schemas.ts, packages/spec/src/data/datasource-credential-redaction.ts, packages/spec/src/data/driver/common.zod.ts, packages/spec/src/data/driver-sql.zod.ts, packages/metadata-protocol/src/metadata-redaction.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata/src/metadata-manager.ts, packages/runtime/src/domains/packages.ts, packages/runtime/src/declared-datasources.ts, packages/services/service-datasource/src/datasource-admin-service.ts, packages/services/service-datasource/src/plugin.ts, packages/services/service-datasource/src/datasource-admin-plugin.ts

Inputs: card #28633 (body and all 8 comments, including the ruling record 6557788232, claim amendments 6557238812 / 6557268469 and the dev's os-dev-report 6558370233), PR #28674 (body, file list, net diff at a2d8e887 against origin/main — main has moved to 2678600d since the merge base 6b9c1ca9 but touched none of the eight PR files nor any cited module, so the net diff is the eight files), the check-runs on the head, and the live tree read at origin/main and pr28674.

Check-runs on the head at the time of reading (41): 13 completed success (Check Changeset, Governed Surface Queue Guard, PR-body guards, Check PR Size, Auto Label, Dogfood Regression Gate, Spec property liveness, Plan Test Core shards, Lint · PM dispatch-gates, Lint · scoped · tooling self-tests, Flag docs affected, filter), 8 skipped (opt-in and matrix shards), and 20 in_progress: Build Core, Build UI Deps, Test Core 1–8, Lint · ESLint / docs & code guards / repo & tooling gates / CI, release & engine gates / scoped ×2, Type Check ×4, Live E2E UI. That is an honest reading, not a verdict on those families: the dev's local run of the 80 derived families (80 × exit 0, 0 NOT-MEASURED) and the full dogfood (VERDICT exit 0) are the only measured answers so far, and queue entry still waits on the CI conclusions. Nothing below depends on them.

① Derived judgments

  1. DatasourceSchema.ssl.key gains .meta({ credential: 'value' }) (datasource.zod.ts). A .meta() adds no validation: the field stays z.string().optional(), the ssl block stays strictObject with the same five members and the same aliases (privatekey / clientkey → key), so the accept set of the published schema is unchanged and Metadata-protocol: none is right. The one public-surface effect is the marker itself, read by the derivation (metadata-credential-fields.ts) and the ratchet; no exported type moves and no generated spec artifact carries the marker (none on main carries webhook's either). Right.

  2. redactDatasourceItem gains a second half (metadata-type-redaction.ts 123–144): the unchanged config rule first, then derivedCredentialRedactorFor(getMetadataTypeSchema('datasource')) applied to the already-config-redacted item. The served answer of every exit that resolves the datasource redactor loses ssl.key when present and names ssl.key in redactedKeys. Read in the tree, the exits that inherit with no code change are: /meta list, item and layered (protocol.ts 9674 / 11000 / 12284–12286 through redactMetadataItem → getMetadataTypeRedactor), getMetaItemsPerPackage on the served face (the body of GET /packages/:id/export, runtime/domains/packages.ts 3230), diffMetaItem (28178), the diagnostics face (metadata-diagnostics.ts 360), the sys_metadata / sys_metadata_history data door (protocol.ts 14062 / 14224, runtime/stored-metadata-reader-seam.ts, service-automation crud nodes, the MCP stdio data bridge 843), MetadataManager.getPublished (metadata-manager.ts 2285–2297), the audit writer and the stored-body migration (plugin-audit), and the realtime publish seam (objectql/engine-realtime-publish.ts 163). So the brief's list — /meta list / item / layered, the data door, getPublished, and the export path — is covered by one lookup, and so are three exits the PR body does not list (package export, diff, realtime publish). Right.

  3. The execution faces stay raw, and the connect path reads them. getMetaItemsForExecution / getMetaItemForExecution and MetadataManager.get / list apply no redactor; every datasource load on the connect and boot path reads those (runtime/declared-datasources.ts 371 metadata.get('datasource'), service-datasource/plugin.ts 181 and 282, datasource-admin-plugin.ts 482 / 487, external-validation-plugin.ts 399). A stored key therefore still reaches the driver factory. Right — the narrowing is a serving act only, as the module's contract (lines 26–31) requires.

  4. The item-by-reference contract holds: with nothing to withhold the input object is returned (return { item, redactedKeys: [] }), so the data door's "stored bytes reach the caller" control and redactStoredMetadataBody's identity short-circuit keep working. redactedKeys is now .sort()ed over both halves, which is what the MetadataRedactionResult docblock already promises ("sorted"). Right.

  5. The ratchet's description rule widens to passphrases? and private[ _-]?keys? (metadata-credential-fields.test.ts 59). Read against every reachable field: the only newly matched position is datasource:ssl.key, now marked. driver-sql.zod.ts 68 (SSLConfigSchema.key, "Client private key …") sits under config, a z.record, which the walk does not enter; the postgres and mysql driver configs declare ssl as a boolean toggle only (driver/common.zod.ts 77) and refuse the certificate-bearing object form inside config; connector-auth.zod.ts 543–555 (appPrivateKey) sits inside the auth union, unreachable; mongo's options description mentions passphrase outside its first sentence and under the config record. So no other registered type reds and no non-datasource type is newly redacted: the derivation acts on markers alone, and the only markers in the tree are webhook's two and this one. Right.

  6. Cleared and absent forms: key: '' is served as written and not named (the module's cleared-form rule); an ssl block without key, or no ssl block, is returned by reference. A key: null would be withheld and named (value === undefined || isClearedForm lets null through to the withholding branch); the carry-forward then grafts null back, so nothing is lost — an observation, not a defect.

  7. An edit that sends the served item back erases nothing. carryForwardRedactedValues (metadata-redaction.ts 486–575) re-runs the redactor on the stored body, walks redactedKeys, and grafts the stored ssl.key wherever the incoming body still carries the ssl container with key absent — exactly the served shape. The GET → edit → PUT pin in protocol.metadata-redaction.test.ts exercises that path through the real save door. A PUT that omits the whole ssl block removes it (author intent, the same posture as config), and key: '' clears it. Right.

  8. The datasource admin door is untouched (packages/services/service-datasource is not in the diff): it reads raw through metadata.get, withholds the key with its own servableSsl (datasource-admin-service.ts 176–181) and carries it forward on edit (958–966), landed by fix(service-datasource)!: the datasource admin doors store ssl, autoConnect and description instead of dropping them, and refuse an inline ssl.key #28648. Its behaviour is unchanged by this PR. See ④ for what that leaves.

② Semver level

  • .changeset/28633-datasource-ssl-key-redaction.md: '@objectstack/spec': patch, Clause-②: no (narrowing), Metadata-protocol: none, the BREAKING banner, and exactly one ADR-0087 marker (not-required (no-migration-prescription)). The PR body carries the same two declaration lines, and the title carries !.
  • Clause-②: no is defined (check-changeset-no-major.mjs 2182–2184) as "widens no published accept set or public surface; add (narrowing) if it narrows". Judged in ①: the diff widens nothing (no new accepted key or value, no new served member, no new export) and narrows one read answer, so no (narrowing) is right; yes (narrowing) was fix(service-datasource)!: the datasource admin doors store ssl, autoConnect and description instead of dropping them, and refuse an inline ssl.key #28648's arm because that PR also added served members, which this one does not.
  • Level: AGENTS.md §3 — (narrowing) is BREAKING; the level follows the protocol kind alone; none takes patch; "a non-protocol change, widening or breaking, ships patch and keeps its BREAKING banner and ADR-0087 disposition". The gate's own self-test names this exact pair (no (narrowing) + none ⇒ patch, none-clean). No authorable key, export or config field is removed or renamed, so no migration mapping or tombstone is owed. Check Changeset on the head: success. patch is right.
  • Only @objectstack/spec publishes a source change; the other five files are tests. No skip-changeset. Right.

③ Boundary flags

Dev deviations (os-dev-report 6558370233):

  1. File surface beyond amendment 6557268469 — datasource.zod.ts (the marker and its docblock) and metadata-credential-fields.test.ts (two words). Both are the order's own preference (derive the set from schema credential markers where the module supports it, and it does), and ruling record 6557788232 already places the packages/spec edit with seat 3 under the maintainer's delegation. Accepted; the seat amends the claim's file-surface line to name the two files.
  2. Seat note asking for the ssl arm in data/datasource-credential-redaction.ts, not taken — correctly not taken. That module derives driver-config credentials from each driver's z.never() contract plus the alias residue (its header and refusedCredentialPaths, 308–366); ssl is a sibling of config, not a member of it, and the driver configs declare ssl as a boolean toggle only. Adding ssl.key there would have been the second hand list the card forbids; the marker is the one declaration, and the ratchet makes a dropped marker red (ablation A2 / A3 in the report).
  3. Fixes #28633, not Part of — matches the ruling record's condition ("Its PR says Fixes #28633"). Consistent.
  4. / 5. The preflight re-run after committing the consumer tests, and the stopped first build — procedural, no contract effect.

Open question (needs_decision): should the spec also refuse an authored inline ssl.key on DatasourceSchema, or keep it accepted and redacted on read?

Answered: A, as this PR does — and the refusal is not owed by this card. Grounds read in the tree, not the PR body: (i) the published spec on main declares ssl.key as "PEM format or path to file"; the path form carries no secret and is the one way a code-defined datasource reaches mutual TLS today, since no sealed channel or ssl key reference is declared and external.credentialsRef is the driver-password channel only. A z.never() refusal (B) would therefore take a declared, delivered capability away at the consumer with no replacement — the shape ④ forbids outright. (ii) The posture A leaves is exactly the one ruling B+ on #24966 already gives webhook.secret: accepted, at rest in the row, withheld on every read exit, carried forward on save. (iii) The admin door's refusal (#28648) is that HTTP door's own posture on a sealed-channel question and binds nothing in spec. (iv) C (a declared secret reference for the client key, then refuse the inline value) is new spec surface with no measured producer: escalated to the spec lane as a follow-up card, to be opened when a producer needs mTLS through a sealed channel, not as a condition on this PR. The cost A names — a /meta-saved or code-defined row may hold key material at rest — is the pre-existing posture of every credential value the B+ derivation covers and is not widened here.

④ Design judgment

Judged over the floor (every non-prose path of the diff, the two schema-face hits first) against the published spec on main.

Does the narrowing take a declared capability away at the consumer? No. The spec declares ssl.key authorable and stored; nothing on main reads ssl.key from a served exit — every connect-path load goes through the raw faces (① 3). The exits that lose it are serving exits, and for each the spec's own invariant (#8154 / #8300, lines 4–5 and 18–34 of the module: "stored credentials must never serve cleartext") already applies to config.password; the diff makes that refusal real for the one ssl member that is key material. ca and cert are public material and stay served, so a reader of the TLS posture loses nothing it can act on. The package export (GET /packages/:id/export) now exports a datasource without its key, as it already exports one without config.password: the same refusal, not a new one. The narrowing makes the spec's own refusal real and retires no declared capability. Holds.

Trying to break it:

  • Marker read off the registry, not the schema. The new half derives from getMetadataTypeSchema('datasource'), and registerMetadataTypeSchema (metadata-type-schemas.ts 343) lets any plugin replace that entry; an override without the marker would serve ssl.key again while the config half still holds, and nothing would say so. No caller in the tree registers a datasource schema (grepped packages/** and ui/**), and the ratchet guards the spec's own copy, so the exposure is hypothetical today — but it is the fail-open shape the module's own note (lines 192–199) names as "the worst available outcome", and the built-in could read DatasourceSchema directly (the sibling registry module already imports it). Should-fix, not a FAIL: pin the built-in half to DatasourceSchema (or memoise on it) rather than on the overridable registry entry.
  • A second declaration of which ssl member is secret. datasource-admin-service.ts 144 (SSL_PRIVATE_KEY = 'key', with servableSsl and the edit carry-forward) is a hand-written twin of the marker, landed by fix(service-datasource)!: the datasource admin doors store ssl, autoConnect and description instead of dropping them, and refuse an inline ssl.key #28648 before the marker existed. Both agree today, and the ratchet protects the spec side; the admin side can drift silently if the schema ever marks a second ssl member (a passphrase, say). The card's "no second hand list" wanted this for the kept-key set, and the same reasoning applies here. Should-fix, follow-up in service-datasource: derive that door's withheld set from credentialFieldsOf(DatasourceSchema) or apply the spec redactor, so one declaration serves both doors.
  • Memoisation. derivedCredentialRedactorFor caches per schema identity; DatasourceSchema is one lazySchema proxy for the process, so the walk runs once and the cache cannot go stale against a static marker. Holds.
  • Ordering with _diagnostics. Unchanged: the consumer computes diagnostics on the raw body before redaction, and the redactor still leaves every non-credential key byte-for-byte. Holds.
  • Ratchet widening reds nothing else (① 5), and ablation A3 in the report shows why the widening is needed (the main rule did not read "Client private key" as a credential). Holds.
  • Snapshot and audit surfaces. sys_metadata_history rows, audit-writer snapshots and the stored-body migration all go through redactStoredMetadataBody and inherit; pinned for history in the data-door test. Holds.
  • The (narrowing) arm's full-dogfood duty — run by the dev (VERDICT exit 0, 2981 passed) and Dogfood Regression Gate completed success on the head. Holds.

Nothing found at must-fix. Two should-fix findings, both follow-ups that do not change what this head serves.

Implemented-by: claude/project-thread-xvyjqe
Reviewed-by: session_01FEkQ5kE5BrPUPNMMyv6oCH

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-project-manager · session_01FEkQ5kE5BrPUPNMMyv6oCH

席位核对 · ACCEPT · PR #28674 · 2026-11-01T13:31Z

Head-sha: a2d8e887a81615b456977522e92d664f280bc80d

维护者速读(席位终稿)

核对六项

  1. 形态:draft → main;Fixes #28633(本 PR 是卡剩下的读出口一半;管理门一半已由 PR fix(service-datasource)!: the datasource admin doors store ssl, autoConnect and description instead of dropping them, and refuse an inline ssl.key #28648 落地),合并关卡正确;闭合关键词另无命中。文件 8 个,在认领修订 6557268469 与 6558513401 申报面内;packages/spec 改动按协调者依维护者授权的裁决(6557788232)由本车道做。
  2. 前提实测:dev 在 main 上实测读出口下发 ssl.key;新增脱敏用例在 main 上红,三条读路径与 getPublished 各有钉子。
  3. 门禁:本 head check-runs 55 success、9 skipped(路径未命中)、0 失败或待跑(复核时在跑的 20 条现已全部成功)。dev 本地:80 条派生门禁全部 exit 0,--ran 0 NOT-MEASURED;9 个消费包测试绿;收窄臂要求的全量 dogfood 绿。
  4. 接受的偏差:复核两条 should-fix 记为后续,不挡落地:(1) 新一段从可被覆盖的注册表 datasource 条目派生,插件覆盖若漏标记会重新下发(树内无此覆盖);(2) datasource-admin-service.ts 的 SSL_PRIVATE_KEY = 'key' 是标记的手写孪生,将来 ssl 多标一个成员时可能漂移。
  5. 范围外去向:dev 的开放问题(spec 层拒收内联 ssl.key)按复核答 A,本 PR 不做;需要 mTLS 的生产者出现时,由 spec 车道立卡(选项 C)。Metadata-protocol: none,Skills-docs: none(已核:无页面陈述读出口下发 ssl 的内容)。
  6. 落地条款:Clause-②: no (narrowing) + packages/spec 已发布 schema ⇒ CONTRACT_REVIEW_TIER 完整复核,本 head PASS 在案(6558503346,Served-tier: CONTRACT_REVIEW_TIER,Head-sha 与现 head 一致)。semver patch 与 changeset 一致(收窄 + 协议 none 取 patch,门禁自判)。check-governed-merges --pr 28674:NOT governed,246 行。落地授权,维护者原话:「按照 pm-dispatch 规则复核过的都授权」(2026-10-25T11:35Z)。

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 13:32
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 13:32
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit f5a400a Nov 1, 2026
65 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-xvyjqe branch November 1, 2026 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:data size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant