Skip to content

docs(drivers): say what the read path withholds from a plugin driver's config - #21953

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-21950-plugin-driver-config-redaction-docs
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-21950-plugin-driver-config-redaction-docs

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21950

Clause-②: no

What

One paragraph in content/docs/data-modeling/drivers.mdx: the plugin-contributed-driver paragraph that #21927 landed. It said a plugin driver's config is "stored and served to administrators as written". The stored half holds. The served half did not: the read path withholds a fixed, name-based set for every driver, contracted or not. The paragraph now names that set and says everything else is served as written. It keeps #21927's intent: the config is unvalidated, keeping secrets out of it is the plugin author's job, and the credential belongs in external.credentialsRef.

Docs only, no code change. The ruling on #21921 stands (no heuristic, no registration API): 「我觉得不需要协议,也不需要改动这么多代码」 and 「同意作废,文档补一句」.

Before (drivers.mdx:157-:160 at 3c7785d4):

The platform also does not guess which of its keys hold credentials: config is stored and served to administrators as written. Keeping secrets out of it is the plugin author's responsibility; put the credential in the bound secret (external.credentialsRef) instead.

After:

... so its config is left unvalidated rather than judged against a shape the platform does not have, and is stored as written. The platform also does not guess which of its keys hold credentials. On read it withholds only what it withholds for every driver: a key named exactly password or authToken, or one of their former aliases (FORMER_CREDENTIAL_ALIASES), and, in a URL-shaped string value, the password in its userinfo and its credential query parameters (CREDENTIAL_URL_QUERY_PARAM_NAMES, such as ?password=). Both apply at every depth of nested objects, but not inside arrays. Everything else in config is served to administrators as written (redactDatasourceConfig in datasource-credential-redaction.ts), and a withheld value still sits in the stored row. Keeping secrets out of config is the plugin author's responsibility; put the credential in the bound secret (external.credentialsRef) instead.

The code the new text rests on (3c7785d4)

Every clause was checked against the code, not taken from the card.

  • Stored as written.
    • packages/spec/src/data/driver/config-registry.zod.ts:435: return id ? DRIVER_CONFIG_SCHEMAS[id] : undefined;
    • config-registry.zod.ts:522: if (!schema) return { known: false };
    • packages/spec/src/data/datasource.zod.ts:693 runs reportDriverConfigIssues(ctx, ds.driver, ds.config, ['config']);, and :485 returns early on if (!result.known) return;.
    • The admin write doors reach the same two judges: datasource-admin-service.ts:1031 (validateDriverConfig) and :1061 (DatasourceSchema.safeParse(record)).
  • Which names are withheld.
    • packages/spec/src/data/datasource-credential-redaction.ts:372: const canonical = derived.length > 0 ? derived : [...CANONICAL_CREDENTIAL_KEYS];. For a driver with no contract, derived is empty.
    • :374: return [...new Set([...canonical, ...FORMER_CREDENTIAL_ALIASES, ...stillWritable])];
    • packages/spec/src/data/driver/common.zod.ts:437: export const CANONICAL_CREDENTIAL_KEYS = ['password', 'authToken'] as const; The alias list starts at :448.
  • Exact name, every object depth, not inside arrays.
    • datasource-credential-redaction.ts:520 builds const hidden = new Set(redactableConfigKeys(driver));, and :527 tests if (hidden.has(key)), an exact-case match.
    • :546-:547 recurse only into value && typeof value === 'object' && !Array.isArray(value). An array falls through to :550, out[key] = value;.
  • URL credentials.
    • :537 runs redactUrlCredentials(value) on every string value.
    • That function (:451) composes redactUrlPassword (:405, which keeps the username) with redactUrlCredentialQueryParams (:436). The second filters on CREDENTIAL_URL_QUERY_PARAM_NAMES (common.zod.ts:311-:312, the union of :299-:300), matched case-insensitively (common.zod.ts:621).
  • Nothing positional for an unknown driver. passthroughSecretPaths returns [] when the id does not resolve (:227). refusedCredentialPaths reads a schema that is undefined (config-registry.zod.ts:435).
  • Where it is served. datasource-admin-service.ts:554 (getDatasource) and packages/spec/src/kernel/metadata-type-redaction.ts:93 (the built-in datasource redactor for the metadata read exits) both call redactDatasourceConfig.
  • The module says the same. datasource-credential-redaction.ts:66-:71 ("canonical spellings are therefore redacted by NAME for unknown drivers too") and :88-:94 ("strips it for EVERY driver").

Measured, not only read. A scratch probe (not committed) ran redactDatasourceConfig('com.vendor.snowflake', …) and DatasourceSchema.safeParse from src at 3c7785d4:

  • The write door accepted the config and stored it byte-equal.
  • The read withheld password, authToken, pwd, token, a nested token, a nested URL's userinfo password and ?password= / ?AuthToken= query pairs.
  • The read served Password (case variant), privateKey, apiKey, clientSecret, a nested secret and a password inside an array element as written.
  • getMetadataTypeRedactor('datasource') reported the same paths under config..

Checks

The diff is one .mdx file and touches no package, so there is no dependency-closure build step for the diff itself. @objectstack/lint and @objectstack/client / @objectstack/client-react were built (with their closures) only because three of the gates read built output.

  • Gate list derived by node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack on the actual diff at 809a8641. It gives the same 40 commands the dispatch named.
  • All 40 exit 0 at 809a8641. Reconciliation: ✓ dispatch-gates --ran: 40 derived famil(ies) accounted for — 40 run, 0 NOT-MEASURED (a DERIVED zero — all 40 recorded an exit code and none of them is 3).
  • pnpm --filter @objectstack/spec run check:skill-examples first exited 3 with PREREQUISITE NOT MET (no client-react declarations). That exit is not a measurement. After building @objectstack/client-react and @objectstack/client it exited 0: ✅ 262 prose examples type-check across 3 surface(s).
  • These were not run locally and are left to CI: the artifact-roster, wide-population and type-check lanes that dispatch-gates lists outside its 40, and the Build Docs job.

Changeset

None (skip-changeset). No published package's files[] ships content/docs. As a probe, the new sentence's text was searched for in every package dist/, in packages/spec/llms.txt, in packages/spec/prompts and in skills/: 0 hits. Positive control: redactDatasourceConfig in packages/spec/dist, 10 files.

Acceptance notes

  • Out of scope, handed to the seat for filing (code, not docs): the read-path still-writable table (STILL_WRITABLE_CREDENTIAL_KEYS) is indexed by the raw driver spelling at datasource-credential-redaction.ts:373. Its sibling passthroughSecretPaths resolves aliases through resolveDriverId (:226-:227), and this lookup does not. A function-level probe shows two symptoms:

    • The turso | libsql row of this page's at-rest table (drivers.mdx:185) holds for one spelling only.
    • A driver id that names an Object.prototype member makes the lookup throw (stillWritable is not iterable). This fails closed.

    One line, one fix. This PR does not touch it.

  • This PR does not change the at-rest-risk section below the paragraph.


Generated by Claude Code

…s config

The plugin-contributed-driver paragraph said `config` is "stored and served
to administrators as written". The write half holds: a driver with no
shipped contract gets no config verdict, so the row is stored as written.
The read half did not: `redactDatasourceConfig` hides the canonical
credential keys (`password`, `authToken`) and their former aliases at every
object depth, plus URL userinfo passwords and credential query parameters,
for every driver, contracted or not.

The paragraph now names that fixed, name-based set, says everything else is
served as written, and keeps the plugin author's responsibility and the
`external.credentialsRef` route. Docs only; no code change.

Claude-Session: https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/s label Oct 6, 2026
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 6, 2026
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Oct 6, 2026
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 05:56
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 05:56
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 01e0f71 Oct 6, 2026
38 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-21950-plugin-driver-config-redaction-docs branch October 6, 2026 06:12
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/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants