Skip to content

Commit 01e0f71

Browse files
docs(drivers): say what the read path withholds from a plugin driver's config (#21953)
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](https://claude.ai/code/session_01VF48aw8RPG6wzDnMgp6rtw)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 76fec88 commit 01e0f71

1 file changed

Lines changed: 12 additions & 4 deletions

File tree

‎content/docs/data-modeling/drivers.mdx‎

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -154,10 +154,18 @@ Two things live **outside** `config`, because they are not driver-specific:
154154

155155
A plugin-contributed driver (`com.vendor.snowflake`) has no contract in this
156156
repo, so its `config` is left unvalidated rather than judged against a shape the
157-
platform does not have. The platform also does not guess which of its keys hold
158-
credentials: `config` is stored and served to administrators as written. Keeping
159-
secrets out of it is the plugin author's responsibility; put the credential in
160-
the bound secret (`external.credentialsRef`) instead.
157+
platform does not have, and is stored as written. The platform also does not
158+
guess which of its keys hold credentials. On read it withholds only what it
159+
withholds for every driver: a key named exactly `password` or `authToken`, or
160+
one of their former aliases (`FORMER_CREDENTIAL_ALIASES`), and, in a URL-shaped
161+
string value, the password in its userinfo and its credential query parameters
162+
(`CREDENTIAL_URL_QUERY_PARAM_NAMES`, such as `?password=`). Both apply at every
163+
depth of nested objects, but not inside arrays. Everything else in `config` is
164+
served to administrators as written (`redactDatasourceConfig` in
165+
`datasource-credential-redaction.ts`), and a withheld value still sits in the
166+
stored row. Keeping secrets out of `config` is the plugin author's
167+
responsibility; put the credential in the bound secret
168+
(`external.credentialsRef`) instead.
161169

162170
<Callout type="info">
163171
The same schemas are projected to JSON Schema for

0 commit comments

Comments
 (0)