Skip to content

feat(spec)!: the connector write door refuses an inline secret on every instance auth kind; the metadata read redacts connector credential positions - #28596

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/harden-connector-credentials
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/harden-connector-credentials

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — skills/** teaches connector auth only as { type, credentialRef } (git grep over skills/**: every auth: example is a reference; none writes token, key or password inline), and content/docs/automation/connectors.mdx already says auth: { type: 'bearer', token: … } is not a legal shape, which this change makes true at every door.

维护者速读

  • 改了什么:连接器实例 auth 的四种静态类型(none / bearer / api-key / basic)不再接受未声明的键,内联的 token / key / password 在保存与构建时以 422 拒绝并指向 credentialRef;元数据读取不再返回连接器的凭据位置(auth 未声明的键、旧 authentication 块除 type 外的键、provider 的请求头与环境变量表整表)。
  • 为什么改:QA 第三轮发现的安全加固(细节按披露裁决不公开)
  • 风险与回滚:在 auth 里多写了键的连接器,保存与构建会被拒;Studio 读到的连接器不再显示 headers / defaultHeaders 等整表(原样保存时存储值保留,运行时照常发送)。回滚即 revert 本 PR。
  • 先看:packages/spec/src/shared/connector-auth.zod.ts(四个 strictObject)、packages/spec/src/integration/connector-credential-redaction.ts(credentialPathsOf 与 declaredConnectorAuthKeys)、packages/spec/src/kernel/metadata-type-redaction.ts(内置注册一行)。
  • 两分钟验证:pnpm --filter @objectstack/dogfood exec vitest run test/connector-meta-door-credential-hardening.dogfood.test.ts,预期 11 个用例全部通过。
  • 需要你做的:无,席位落地

What changes

Write door. The four static kinds of declarative connector instance auth (none, bearer, api-key, basic) are strictObject shapes now, as the three ADR-0143 kinds (oauth2, app-credential, signed) already were. An undeclared key is refused at parse, located at auth, and an inline secret (token, key, password and their usual spellings) meets the credentialRef prescription. That is the refusal ADR-0143 D3 records ("publish keeps refusing an inline secret") and its Acceptance asks for on any variant. Every door that parses DeclarativeConnectorEntrySchema refuses it: defineStack({ connectors }), os validate / os compile, and PUT /api/v1/meta/connector/:name, where the answer is 422 INVALID_METADATA, the envelope an inline authentication already met there. No error code, export or authorable key is added.

Read redaction. connector joins datasource as a built-in of the metadata-type redactor registry (getMetadataTypeRedactor('connector')). It is a built-in because connector rows reach the save door with or without any connector plugin mounted. The withheld positions are:

  • every auth key the item's kind does not declare (read from ConnectorInstanceAuthSchema itself, so a new kind is covered with no edit);
  • every key of a runtime authentication block but type;
  • the provider header and environment maps, each whole: providerConfig.headers, providerConfig.defaultHeaders, providerConfig.transport.headers and providerConfig.transport.env. This follows the posture ruled for a webhook's headers, since a map's entries cannot be told apart.

Cleared forms (null, '', {}) are served as written. The redactor is pure and internal (not exported). The runtime keeps reading the unredacted execution view, and the protocol's generic carry-forward keeps stored values on a save of the served form.

ADR-0087. A D3 semantic entry, connector-instance-auth-inline-secret-refused, carries the prescription. There is no mechanical rewrite, because the inline value may be the only copy of the secret its author holds.

Tests

Every value in the new tests is an obviously-fake placeholder, and every read assertion is that value's absence from what a door served.

  • Spec pins. connector-auth-static-kinds.test.ts: each static kind's declared shape parses unchanged (control), each inline secret is refused as unrecognized_keys @ auth with the credentialRef prescription and without echoing the value, a non-secret undeclared key is refused too, and the stack collection refuses at connectors.0.auth. connector-credential-redaction.test.ts: each withheld position; declared keys served as stored (grant and signed kinds included); cleared forms; purity; and a fail-closed census of every key the instance-auth union declares, so a new declared key reds until it is confirmed not a secret. metadata-type-redaction.test.ts: connector resolves with zero registrations, and the stored-row seam redacts both type spellings.
  • Dogfood. connector-meta-door-credential-hardening.dogfood.test.ts, on the showcase with its own connector plugins, 11 tests:
    • WRITE: each static kind answers 422 INVALID_METADATA located at auth and stores nothing.
    • CONTROL: a credentialRef-only instance saves, reaches ready, and dispatches to a local upstream with the reference resolved and its default header sent, also after a save of its served form.
    • READ: a row seeded through the system context is served withheld to a member and to an administrator, by name, in the layered view, in the list (both spellings) and through the protocol's reads. Every other field is served as stored, and the stored row is untouched.
Readings at 88503d9 (before the main merge)
spec  pnpm test                782 files passed, 23111 tests passed (1 todo)        exit 0
spec  typecheck                                                                     exit 0
dogfood typecheck                                                                   exit 0
dogfood connector-meta-door-credential-hardening   11/11                            exit 0
service-automation src/connector-*   19 files, 256 tests                            exit 0
objectql protocol-meta + manifest-service-residual-objects   153 tests              exit 0
metadata-protocol redaction suites   67 tests                                       exit 0
runtime automation-connector-credential-door   17 tests                             exit 0
connector-rest 43 · connector-openapi 92 · connector-mcp 74                         exit 0
connector-app-github 10 · connector-app-stripe 16 · cloud-connection refs 4         exit 0
lint flow-credential-literals 17 · cli --project unit 360 files, 5150 tests         exit 0
eslint --no-inline-config over the 8 touched files                                  exit 0
spec check:generated: all 14 generated artifacts up to date                         exit 0
Ablations (one shot each; scripts/ablation-replace.mjs + ablation-dist-preflight.mjs; restored to the HEAD blob)
A  bearer kind back to a plain z.object (marker in dist/: 2 files)
   predicted: only the bearer WRITE case red        observed: 1 failed | 10 passed (bearer: 200, expected 422)
   (a first run before each kind had its own item name read 4 red: bearer on the status, the
    other three only on "nothing is stored" because the bearer row persisted under the shared
    name; the names were split and A was run once more, declared)
B  connector built-in replaced by a redactor that withholds nothing (marker in dist/: 4 files)
   predicted: the 4 served-body tests red           observed: 4 failed | 7 passed
   (a first attempt aborted before its test ran: a cross-marker preflight refused on the
    mutated tree; not a run)
restore: blob == HEAD for both files, whole-tree status clean, both markers absent from all
588 built files, restored run 11/11
Readings at 41256e9 (after the main merge; the final head)
closure build (turbo, @objectstack/dogfood^...)   68/68 tasks                        exit 0
spec check:generated: all 14 generated artifacts up to date                         exit 0
spec  pnpm test                782 files passed, 23122 tests passed (1 todo)        exit 0
spec  typecheck · dogfood typecheck                                                 exit 0
service-automation src/connector-*   20 files, 261 tests                            exit 0
objectql protocol-meta + manifest-service-residual-objects   153 tests              exit 0
metadata-protocol redaction suites   67 tests                                       exit 0
runtime automation-connector-credential-door   17 tests                             exit 0
connector-rest 43 · connector-openapi 109 · connector-mcp 102                       exit 0
connector-app-github 10 · connector-app-stripe 16 · cloud-connection refs 4         exit 0
lint flow-credential-literals 17 · cli --project unit 362 files, 5155 tests         exit 0
full dogfood battery (pnpm --filter @objectstack/dogfood test, under the lock)
  Test Files 365 passed | 3 skipped (368) · Tests 2945 passed | 28 skipped (2973)  exit 0
  (the new file is in the battery's isolated population: vitest list --filesOnly)

Acceptance notes

  • Rows stored before this change. The read withholds their positions, and the stored value stays at rest. A save of the served form keeps it: the protocol's carry-forward, unchanged here. Such a row's _diagnostics now names the undeclared auth key, which is the inventory for moving the value out. Moving it is the stored-row migration's job, the datasource posture.
  • Header and environment maps are withheld whole. Studio's connector reads no longer show headers / defaultHeaders / transport.headers / transport.env. A save that omits a map keeps it; an edit sends the whole map. Readers of the served view (the package export among them) omit these maps, as they already omit a webhook's headers.
  • auth: { type: 'none' } refuses credentialRef as well: it names an upstream that takes no credential.
  • Generated artifacts. None moved: the strictness ledger's shared/ total, the authorable surface, the JSON schemas and the api-surface are byte-identical (check:generated).
  • Scope. The instance auth positions, the runtime authentication block and the provider maps. Read gating of /meta/connector is unchanged.

Verification

Local subset at 41256e9. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands derived 82 families. Each ran byte for byte with its exit code captured before any pipe, followed by check:doc-anchors and check:system-context-census, which the dispatch names. Reconciliation result: --ran: 82 derived, 82 run, 0 NOT-MEASURED, 0 UNRUN.

Two families first answered exit 3 (prerequisite not met), so nothing was measured, and each was re-run once:

  • check:dual-build-cjs-loads re-ran after building the seven packages outside the dogfood closure, and exited 0 (115 entry points across 70 packages load).
  • check:type-check-debt re-ran under the verify lock and exited 0 (0 ledger entries above their recorded number).

Every other family exited 0 on the first run.

CI on 41256e9: 64 check-runs, none other than success or skipped. The required contexts Lint & Repo Gates, TypeScript Type Check, Test Core, Build Core, Governed Surface Queue Guard, Type Check UI, Test UI, Build & E2E UI and Live E2E UI all succeeded; Lint UI was skipped, because the diff touches no ui/ path. Dogfood Regression Gate and Check Changeset also succeeded.


Generated by Claude Code

…y kind; the metadata read redacts connector credential positions

The four static instance-auth kinds (none, bearer, api-key, basic) are
strict shapes now, as the ADR-0143 kinds already were: an undeclared key is
refused at parse, and an inline secret meets the credentialRef prescription.
A built-in connector redactor withholds undeclared auth keys, the non-type
keys of a runtime authentication block, and the provider header and
environment maps on every metadata read exit.

Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk
Co-authored-by: Claude <noreply@anthropic.com>
…the metadata doors; changeset

Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk
Co-authored-by: Claude <noreply@anthropic.com>
…s only its own write

Claude-Session: https://claude.ai/code/session_017xRBcVy4VXmkQjn2MGeaWk
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/l documentation Improvements or additions to documentation tests tooling 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 24 documentable anchor(s).

4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/automation/connectors.mdx (via defaultHeaders (literal, a string literal in CONNECTOR_CREDENTIAL_MAP_PATHS), providerConfig (literal, a string literal in CONNECTOR_CREDENTIAL_MAP_PATHS), /api/v1/automation/connectors (route, a path literal in acceptanceCriteria), /meta/connector/:name (route, a path literal in surface))
  • content/docs/data-modeling/external-datasources.mdx (via providerConfig (literal, a string literal in CONNECTOR_CREDENTIAL_MAP_PATHS))
  • content/docs/data-modeling/odata-datasources.mdx (via providerConfig (literal, a string literal in CONNECTOR_CREDENTIAL_MAP_PATHS))
  • content/docs/kernel/cluster.mdx (via /api/v1/automation/connectors (route, a path literal in acceptanceCriteria), /meta/connector/:name (route, a path literal in surface))

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

  • content/docs/releases/v15.mdx (via providerConfig (literal, a string literal in CONNECTOR_CREDENTIAL_MAP_PATHS), /api/v1/automation/connectors (route, a path literal in acceptanceCriteria))
  • content/docs/releases/v17/17-6.mdx (via /meta/connector/:name (route, a path literal in surface))

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
  • 5 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 213 client-bound route-ledger rows — the other 153 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 153: 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; 98 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 cd065d3698db13969a28f46bf26f3e47e9ecc09c → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8a12c36d9c75026b9b6c57f765ed033556ecbc9c — the merge of head 41256e95b9caece74a323a166252f4ce52975732 into base cd065d3698db13969a28f46bf26f3e47e9ecc09c, 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 8a12c36d9c75026b9b6c57f765ed033556ecbc9c && git checkout 8a12c36d9c75026b9b6c57f765ed033556ecbc9c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin cd065d3698db13969a28f46bf26f3e47e9ecc09c 41256e95b9caece74a323a166252f4ce52975732 && git checkout -B drift-repro cd065d3698db13969a28f46bf26f3e47e9ecc09c && git merge --no-ff 41256e95b9caece74a323a166252f4ce52975732

node scripts/docs-audit/affected-docs.mjs --json cd065d3698db13969a28f46bf26f3e47e9ecc09c

⚠️ 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 cd065d3698db13969a28f46bf26f3e47e9ecc09c → 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:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 41256e95b9caece74a323a166252f4ce52975732
Local-runs: none
Scope: packages/spec/src/integration/connector-credential-redaction.ts, packages/spec/src/kernel/metadata-type-redaction.ts, packages/spec/src/migrations/entries/semantic/18.connector-instance-auth-inline-secret-refused.ts, packages/spec/src/shared/connector-auth.zod.ts, packages/spec/src/integration/connector-auth-static-kinds.test.ts, packages/spec/src/integration/connector-credential-redaction.test.ts, packages/spec/src/kernel/metadata-type-redaction.test.ts, packages/qa/dogfood/test/connector-meta-door-credential-hardening.dogfood.test.ts + packages/spec/src/integration/connector.zod.ts, packages/spec/src/shared/strict-object.ts, packages/spec/src/kernel/metadata-credential-fields.ts, packages/spec/src/kernel/metadata-type-schemas.ts, packages/spec/src/stack.zod.ts, packages/spec/src/system/environment-artifact.zod.ts, packages/spec/src/migrations/entries/semantic/17.connector-inline-authentication-publish-refused.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/metadata-redaction.ts, packages/metadata-protocol/src/protocol-credential-carry-forward.ts, packages/metadata/src/metadata-manager.ts, packages/metadata/src/plugin.ts, packages/runtime/src/domains/packages.ts, packages/services/service-automation/src/plugin.ts, packages/cli/src/commands/compile.ts, docs/adr/0143-outbound-connector-auth-is-open-tier.md, docs/adr/0087-metadata-protocol-upgrade-contract.md, scripts/check-changeset-no-major.mjs, scripts/check-adr-0087-registration.mjs

Face: a published schema (packages/spec/src/**) plus a declared narrowing, so the floor is every path of the diff outside the changeset; the net diff against main (merge-base 617dcf432) is the nine files the API lists, 914 insertions, 10 deletions. Reviewed from the tree, not the PR body. Per the disclosure ruling this record names positions and refusals only.

① Derived judgments

  1. The widened refusal set — right. The four static kinds (none, bearer, api-key, basic) move from z.object to strictObject, which is closedObject(z.object(shape, { error }).strict()): the unknown-key refusal is terminal on every zod line, so a body carrying an undeclared key raises exactly one unrecognized_keys issue at auth and no second complaint from the entry's superRefine. The refused set is every key the kind's shape does not declare, not a denylist: an inline secret in any listed spelling, a non-secret key borrowed from another kind (headerName on bearer), and credentialRef on none. That is what ADR-0143 D3 ("publish keeps refusing an inline secret") and its Acceptance ("an inline secret on any variant is refused at publish with a located error") ask for, and what the changeset declares. The guidance keys on each shape are keys the shape rejects, so the alias-integrity audit holds; the message names the key and never the value.
  2. The doors — right, with one door the changeset does not name. DeclarativeConnectorEntrySchema is reached by: defineStack (stack.zod.ts, connectors: z.array(DeclarativeConnectorEntrySchema)); os validate, os compile, os lint score, scaffold-validate, author-time-rules and os migrate meta (each a safeParse of ObjectStackDefinitionSchema); PUT /api/v1/meta/connector/:name (metadata-type-schemas.ts registers connector to this schema; the save door's gate safeParses the author's body and mints 422 INVALID_METADATA with structured issues; the dogfood pins the issue located at auth on all four kinds and that nothing is stored); Studio's client validation (clientValidation.ts resolves the same schema). A fourth door answers identically and is not named: the metadata plugin's artifact load at boot and on reload (_parseAndRegisterArtifact: ObjectStackDefinitionSchema.parse for a bare definition, and EnvironmentArtifactSchema whose metadata is ObjectStackDefinitionSchema). No artifact os build emitted carries an undeclared key, because compile.ts serializes result.data, the parsed stack, which the old z.object already stripped; only a hand-assembled artifact JSON would now refuse at boot where it stripped before. Not a defect; see ③ for the prose note.
  3. Previously stored rows keep loading and materializing — right. No boot or reload path parses a sys_metadata row through the type schema: the protocol's overlay hydration registers the raw row, the one schema use on reads is _diagnostics, and nothing in runtime, rest, metadata-protocol or service-automation gates on _diagnostics.valid. The automation plugin reads registry.listItems('connector') and getMetaItemsForExecution (audience execution, no decoration), so a row holding an undeclared auth key still reaches the provider with credentialRef resolved as before; its served _diagnostics now reads valid: false naming auth, the inventory the changeset describes.
  4. Carry-forward on a save of the served form — right. In saveMetaItem the schema gate judges the author's body first and carryForwardRedactedCredentials runs after every gate, immediately before the put; it re-grafts each withheld path only where the incoming body is indistinguishable from the served form, then runs the type's redactor over the grafted body and keeps a value only where the redactor still withholds it. So a label edit of a legacy row re-saves and keeps its inline value at rest, a body that writes token itself is refused on its own merits, and an explicit defaultHeaders: {} is the author's word (a cleared form is served as written, so it differs from the stored map and wins). The dogfood CONTROL pins the defaultHeaders round trip and the dispatch after it.
  5. The redactor reaches every read exit — right. connector is a built-in beside datasource; precedence is explicit registration, then built-in, then the schema derivation, and the connector schema carries no .meta({ credential }) marker, so no derived coverage is bypassed. Every consumer resolves through getMetadataTypeRedactor: the protocol's decorateMetadataItems (behind getMetaItems, getMetaItem, and getMetaItemsPerPackage, which GET /packages/:id/export reads), the /layers read (code, overlay and effective bodies), the diff views, redactStoredMetadataBody / redactStoredMetadataRow(s) (the generic data door on sys_metadata and sys_metadata_history, the runtime reader seam, the MCP stdio bridge, the audit writers, the flow CRUD nodes) and MetadataManager.getPublished. The dogfood pins by name, /layers, both list spellings and the protocol's reads, for a member and an administrator.
  6. Fail-closed behaviour — right, one gap. A type naming no kind, or no type, leaves type alone declared; an unknown grant or scheme keeps the keys its kind's siblings declare, none of which is a secret (the census pins the sixteen declared keys); a non-map value at a map position is withheld; a connector row whose body column will not parse as JSON now has its body withheld at the stored-row seam, where it was served as stored before (the type had no redactor). The gap: a non-object auth or authentication value (a string) is served as stored. No authoring door stores that shape (every door refuses it as invalid_type, before and after this change); only a system-context write reaches it. Should-fix, follow-up, in ④.
  7. Public surface unchanged — right. connector-credential-redaction.ts is re-exported by no index.ts, and the package exports map has no wildcard subpath, so redactConnectorItem, declaredConnectorAuthKeys and CONNECTOR_CREDENTIAL_MAP_PATHS are shipped bytes, not published surface; metadata-type-redaction.ts exports the same names as before. The api-surface, declaration-map and export-origins baselines are untouched in the diff and check:api-surface is in the green Lint jobs.
  8. Generated artifacts consistent — right. The strictness ledger shard counts object sites per directory from the AST (shared/ = 26) and a z.object to strictObject swap at the same sites moves no count; the authorable-surface baseline records keys, the json-schema manifest records schema names, neither records posture. check:generated and check:strictness-ledger sit in the green Lint jobs on this head.
  9. Metadata-protocol narrowed on the read half too — right. /meta/connector and the protocol's reads now serve a body without positions they served before (undeclared auth keys, authentication keys but type, the four maps whole). That is a served-shape narrowing and the Metadata-protocol: narrowed line covers it.

② Semver level

@objectstack/spec: minor with a BREAKING banner, Clause-②: no (narrowing) on the PR body and the changeset, Metadata-protocol: narrowed, a FROM → TO block, and adr-0087: registered connector-instance-auth-inline-secret-refused. The level axis of check-changeset-no-major.mjs asks a narrowed declaration to grade at least one package whose published source moves minor or above and to sit beside the (narrowing) arm: both hold. The registered id resolves to entries/semantic/18.connector-instance-auth-inline-secret-refused.ts, whose four fields mirror the sibling 17.connector-inline-authentication-publish-refused.ts; check-adr-0087-registration is in the green Lint jobs. Precedent: #28496 (044d2016a) declared a @objectstack/spec accept-set narrowing the same way (minor, (narrowing), registered). The door-only narrowings in other packages that took patch are not this case: here a published schema's accept set moves and a semantic migration entry is owed. Declaration correct.

③ Boundary flags

The PR author is objectstack-fleet[bot], so no author note is owed.

  • (1) Credential-looking values in headers / defaultHeaders at write, no refusal. Accept as out of scope. A header value is a string the provider sends; refusing by shape would be a heuristic, and lint-flow-credential-literals is the precedent for an advisory rule. The read now withholds the maps, so such a value is at least no longer served. Follow-up card: an advisory os lint rule, not a door refusal.
  • (2) Read gating of /api/v1/meta/connector for members unchanged. Escalate as a posture question, not blocking. The dogfood pins a member reads a connector 200 with every credential position withheld; the served body still carries credentialRef names, baseUrl and transport.url. Nothing here widens that read.
  • (3) Undeclared top-level connector keys stored and served. Accept as out of scope. ConnectorBaseSchema is deliberately non-strict (its retirements are retiredKey() tombstones under ADR-0104), and a top-level key is not a credential position ADR-0097 §3 or ADR-0143 D3 names. If the shape is to close, that is the 未知键静默剥离仍是全仓默认:把 #3405 的 strict 收紧从一个 schema 推广到整个可授权面(ADR-0078 完整性闸门) #4001 campaign's own ledger row, not this change.
  • (4) Maps withheld whole, non-credential headers included. Accept: the posture ruled for a webhook's headers (credential: 'map'), and a map's entries cannot be told apart. Consequence to name on a card: GET /packages/:id/export reads the served view, so an exported package re-imported elsewhere loses headers, defaultHeaders, transport.headers and transport.env, exactly as it loses a webhook's headers today. Follow-up, not blocking.
  • (5) Rows with an inline value at an undeclared auth key keep it at rest via carry-forward. Accept: the datasource posture ([services half of #7990] Close the datasource/connector credential write/read paths: scrub getDatasource().config, fix the false "credential-stripped" claim, and write the stored-cleartext-rows migration story #8081 item 3), and the carry-forward never creates cleartext; _diagnostics is the inventory. Follow-up: the stored-row migration that moves such a value into the credential store, filed from the private report, not described publicly.
  • (6) URL-borne credentials in providerConfig.baseUrl / transport.url not redacted. Follow-up, not a must-fix on this PR: the gap predates it and nothing here widens it, and the datasource redactor's URL scrub shows the shape (scrub on read, carry forward on save). File it from the private report at the severity the QA report gives it.
  • Boot door (from ① item 2). Should-fix on the prose: one sentence in the changeset that the artifact load at boot and reload parses the same schema, and that an artifact os build wrote carries no undeclared key. Not blocking.

④ Design judgment

  • packages/spec/src/shared/connector-auth.zod.ts — holds against ADR-0143 D3 / Acceptance and ADR-0097 §3, and against its first consumer: ConnectorInstanceAuthSchema is a discriminatedUnion('type'), so a body routes to one member and the member's terminal refusal is the one issue at auth; the superRefine on the entry does not add to it. The shared history sentence is true of the four kinds. Tried: a spelling outside guidance (apiKey on bearer) is still refused, with the distance fallback and no suggestion that lands on a tombstone; none with credentialRef is refused with a prescription naming a kind that takes one. No finding.
  • packages/spec/src/integration/connector-credential-redaction.ts — holds against the registry contract (pure, returns the input by reference when nothing is withheld, sorted dotted redactedKeys, _diagnostics untouched) and against the carry-forward's path grammar (key hops only, no arrays). Reading the declared keys from the union through wrapperChainOf / objectShapeOf / zodDefOf covers a kind added later and both nested unions. Two should-fixes, both follow-ups: (a) a non-object auth or authentication is served as stored (① item 6); withholding a non-record at those two positions, as the map positions already do, closes it in a line. (b) CONNECTOR_CREDENTIAL_MAP_PATHS names four provider-owned positions by hand; a provider plugin that introduces another header or environment map under providerConfig is not covered, and the census guards only the auth kinds. A provider-declared credential-position contract is the durable form; a card, not this PR.
  • packages/spec/src/kernel/metadata-type-redaction.ts — one registration line and its note; holds. The built-in placement is the measured fail-open argument the module already makes for datasource, and listMetadataTypeRedactorTypes lists it. No finding.
  • packages/spec/src/migrations/entries/semantic/18.connector-instance-auth-inline-secret-refused.ts — holds: the id is the one the changeset marker names, the four fields follow the sibling entry, the prescription matches the shapes as declared (api-key with optional headerName / paramName, basic with username), and the acceptance names a route that exists (GET /api/v1/automation/connectors, the ready state the dogfood waits on). No finding.
  • The three spec test files and the dogfood file were read, not reviewed: every value is a placeholder and every read assertion is an absence; the dogfood seeds the stored row through the system context and pins that the stored row itself is never redacted.

Must-fixes: none. Should-fixes (follow-up cards, none blocking): the non-record auth / authentication fail-closed line; the provider-declared credential positions contract; the export round trip losing provider maps; the URL-borne credential scrub; the boot-door sentence in the changeset.

Implemented-by: claude/harden-connector-credentials
Reviewed-by: session_017xRBcVy4VXmkQjn2MGeaWk

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:devx#1 · sunhaolin · session_017xRBcVy4VXmkQjn2MGeaWk

ACCEPT → head 41256e95b9caece74a323a166252f4ce52975732 · 2026-11-01T08:40Z

维护者速读

  • 改了什么:连接器实例 auth 的四种静态类型(none、bearer、api-key、basic)改为严格结构,内联的 token、key、password 在保存和构建时以 422 拒绝,并提示改用 credentialRef。元数据读取不再返回连接器的凭据位置:auth 中未声明的键、旧 authentication 块里除 type 以外的键,以及 provider 的请求头表和环境变量表(整表隐藏)。
  • 为什么改:QA 第三轮发现的安全加固(细节按披露裁决不公开)。
  • 风险与回滚:BREAKING 收窄(spec minor,ADR-0087 D3 条目 connector-instance-auth-inline-secret-refused)。auth 里多写了键的连接器,保存和构建会被拒。Studio 与包导出读到的连接器不再带请求头表和环境变量表;原样保存时存储值保留,运行时照常发送。回滚即 revert 本 PR,不涉及数据迁移。
  • 先看:packages/spec/src/shared/connector-auth.zod.ts、packages/spec/src/integration/connector-credential-redaction.ts、packages/spec/src/kernel/metadata-type-redaction.ts。
  • 两分钟验证:pnpm --filter @objectstack/dogfood exec vitest run test/connector-meta-door-credential-hardening.dogfood.test.ts,预期 11/11 通过。
  • 需要你做的:无,席位落地。读取门控、请求头凭据提醒、顶层未声明键这三个后续问题,已在 PM 会话里交给维护者裁决,不影响本 PR。

席位意见:收。本 head 上达档契约复核 PASS(6555639907,CONTRACT_REVIEW_TIER),0 个 must-fix。

席位核对

  1. 形态:draft,目标 main。按披露裁决不建卡,正文没有关闭关键词。正文由席位按 dev 暂存稿写入,回读一致;首行是签名,Clause-②: no (narrowing)、Metadata-protocol: narrowed、Skills-docs 三行都在。
  2. 前提实测:dev 在 37184244b 上复现了写入和读取两半问题,探针用完即删。两组 ablation 都会变红:写入那组只有 bearer 变红,读取那组恰好 4 个返回体用例变红;恢复后 11/11 通过,dist 中两个标记都已清除。
  3. 门禁:本地 dispatch-gates 共 82 项,全部 exit 0。完整 dogfood 跑了 365 个文件,0 失败。本 head CI 共 68 项:57 success、11 skipped、0 失败;Lint & Repo Gates、TypeScript Type Check、Test Core 都是 success,Check Changeset 两次运行都是 success。
  4. 契约面:碰到已发布 schema(packages/spec/src/**),并声明了收窄 → 按 CONTRACT_REVIEW_TIER 做完整的 ①–④ 复核,结果 PASS。复核确认四点:
  5. 范围外去向:
    • 复核 should-fix 第 5 条:changeset 漏写了"启动时加载 artifact"这一入口,但它的行为与已写的入口一致,不构成错误陈述,席位接受现状;
    • 其余 should-fix 和 dev 带回的 open question 都是后续事项。涉及凭据的部分按披露裁决由 PM 私下处理,不上 GitHub。
  6. 落地:不属于受管面,924 行,低于 3000 行上限 → pr_ready + automerge_enable。

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 1, 2026 09:41
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge November 1, 2026 09:41
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 672bda0 Nov 1, 2026
69 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/harden-connector-credentials branch November 1, 2026 09:58
This was referenced Nov 1, 2026
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 size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant