Repository navigation
docs(drivers): a plugin driver's config is its author's to keep free of credentials - #21927
Merged
objectstack-fleet[bot] merged 1 commit intoOct 6, 2026
Conversation
…of credentials Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
objectstack-fleet
Bot
deleted the
claude/docs-plugin-driver-config-credentials
branch
October 6, 2026 01:44
This was referenced Oct 6, 2026
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…jectstack-ai#21943) Part of objectstack-ai#21932 Clause-②: no ## What changes The platform checklist gains items for the rules the 17.7 pre-release security follow-up landed, and two re-checks from the card are resolved. All edits are in `docs/qa/platform-checklist/areas/*.json`. `automation.json` is untouched (open PR objectstack-ai#21928 holds it). | Card row | Disposition | Item | |---|---|---| | objectstack-ai#21792 (PR objectstack-ai#21809) settings audit and secret-valued settings | new item | `platform-core.settings-audit-secret-fingerprint` | | objectstack-ai#21846 (PR objectstack-ai#21872) implicit account linking | new item | `identity-auth.implicit-account-linking-ownership` | | objectstack-ai#21839 (PR objectstack-ai#21890) share-link password | three clauses added, rev 4 to 5 | `access-security.share-link-capability-tokens` | | objectstack-ai#21836 (PR objectstack-ai#21879) global search skips unreadable objects, plus the two cases objectstack-ai#21880 lists | new item | `search.global-search-skips-unreadable` | | re-check 1: A2 / A7 and the plugin-driver boundary | rev 2 to 3 | `integration-system.datasource-credential-refusal-matrix` | | re-check 2: the objectstack-ai#21845 CLI and quorum N1 notes | already applied by objectstack-ai#21891, no edit | `cli.scaffold-first-run`, `cli.scaffold-console-first-paint`, `approvals.quorum-m-of-n` | Each item states rules, not reproductions. Withheld security detail stays out. ### Grounding, per row - **Settings audit fingerprint.** Both ledgers record the keyed digest for a secret-valued setting, or no fingerprint when none is available, and never the value or an unkeyed hash. Grounded in `settings-service.ts#secretAuditDigest`, `config-change-audit.ts#CONFIG_CHANGE_ACTION` and the contract text at `crypto-provider.ts#keyedDigest`. The pin is `settings-audit-secret-digest.test.ts` (7 cases). The offline check carries a positive control: the non-secret key's unkeyed digest IS found, so a no-hit on the secret rows means something. The no-keyed-digest arm cannot be reached on a stock boot, so that clause is scored from the pin. - **Implicit account linking.** Four rules: no implicit link to an unverified local user; an unlink is honoured; an explicit, signed-in link still works and lifts the refusal; the platform IdP exception holds only on its OAuth path. Grounded in `implicit-account-linking.ts` (`decideImplicitLink`, `IMPLICIT_LINK_REFUSED`, `PLATFORM_IDP_PROVIDER_ID`, `recordUnlinkTombstone`, `refuseImplicitAccountLink`) and the published `sso.mdx` section. The pin is `implicit-account-linking.test.ts`. The item reuses the local OIDC provider recipe from `identity-auth.linked-accounts-social`. The platform-IdP clause and the operator override are pin-scored, and knownGaps says why. - **Share-link password.** The stored hash leaves on no exit (mint, list, redemption). The password is accepted from the `X-Share-Password` header, the query form is still accepted, and the default CORS allow-list carries the header. Both public routes answer `Cache-Control: no-store` and `Vary: X-Share-Password` on every outcome, and the authenticated routes do not. Grounded in `share-link-service.ts#withoutPasswordHash`, `share-link-routes.ts#SHARE_LINK_PUBLIC_RESPONSE_HEADERS`, the runtime `share-links.ts#PUBLIC_RESPONSE_HEADERS` and `adapter.ts#DEFAULT_CORS_ALLOW_HEADERS`. The pins are the `[objectstack-ai#21839]` blocks in `share-link-password.test.ts`, `share-links-public-cache-headers.test.ts` and the hono-plugin CORS case. Existing clause indices are unchanged. - **Global search.** An unreadable object is never queried, named or counted. An explicit `objects=` naming one answers exactly as a name that matches no object. The object stays refused at its own door. Row scope still narrows a searched object, and a term found only in a field hidden from the caller yields no hit. Grounded in `protocol.ts#searchAll` (the `canReadObject` pre-filter and the `getQueryableFields` narrowing). The pins are the dogfood `search-skip-unreadable.dogfood.test.ts` and the 12 unit cases in `protocol.search-skip-unreadable.test.ts`. The two objectstack-ai#21880 cases have no end-to-end pin yet, and knownGaps says so. The open pinyin-companion finding on objectstack-ai#21880 is recorded as a knownGap with a flag-off instruction, at class level only. The persona reuses the area recipe `qa-contributor-bound-member`. - **Datasource credential matrix.** A2 / A7 (`acceptance[1]` and `acceptance[6]`) are recorded as a known environment gap. They need a reachable credential-protected database of a shipped driver, which no run has had. No recipe is claimed, because none is proven. A successful publish alone may not score them, and the stored-credential half of A7 can be read as a partial reading. Separately, the unknown-driver clause, step 7, its negative and the title now state the ruled boundary from objectstack-ai#21921 and the docs note objectstack-ai#21927. For a plugin driver, only the fixed spellings are redacted (the canonical keys, the former aliases and URL credentials). A non-canonical key served as written is the boundary, not a FAIL. Grounded in `common.zod.ts#CANONICAL_CREDENTIAL_KEYS` and `datasource-credential-redaction.ts#redactableConfigKeys`. ### Re-check 2 evidence (no edit) At the claim ref `9dce635337`: - `cli.scaffold-first-run` (rev 3) step 0 and `cli.scaffold-console-first-paint` (rev 3) step 0 both drop the trailing `npm install` and warn against adding it. Their rev 3 history entries cite objectstack-ai#21845. No other `npm install` step remains in `cli.json`. - `approvals.quorum-m-of-n` (rev 4) `negative[0]` requires a NON-PRIVILEGED repeat actor and names the documented admin override (objectstack-ai#3424) as never a distinctness FAIL. ## Remaining on objectstack-ai#21932 (held, not in this PR) - The objectstack-ai#21864 row (public-form withdrawal layering). Its PR is still open. - The objectstack-ai#21928 row (run-state trigger record mask). That PR adds its own item in `automation.json`. objectstack-ai#21932 remains open for these two rows. ## Validation (at `a72b827e43`) - `pnpm check:platform-checklist`: exit 0. It reports 15 areas and 273 items (269 active, 2 planned). The baseline was 270. Symbol anchors resolve 674 of 684 (baseline 657 of 667): all 17 new anchors resolve, and the objectstack-ai#16898 residual is unchanged at 10. - `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived 13 commands, and all 13 exit 0. `check:doc-formula-expressions` first exited 3 (PREREQUISITE NOT MET: `@objectstack/formula` and `@objectstack/lint` were not built). After building them it exited 0. `--ran` reconciliation: 13 derived, 13 run, 0 unrun. - No package source changed, so there is no package build, test or typecheck. No changeset: `docs/qa/**` publishes nothing. ## Acceptance notes - Source citations name test cases and symbols, never line numbers, because `check:platform-checklist` refuses a `file:line` pin. - `content/docs/data-modeling/drivers.mdx` says a plugin driver's `config` is "stored and served to administrators as written". The read redactor still withholds the canonical spellings (`password`, `authToken`), the former aliases and URL credentials for such a driver (`redactableConfigKeys`). So the docs sentence is slightly broader than the code, and the code is the more protective of the two. The checklist follows the code. This is noted only, with no card. Carrier: none. - A run of `search.global-search-skips-unreadable` picks the walled object and the hidden-field value on the live boot, behind premise guards. The item names likely candidates and does not assume them. --- _Generated by [Claude Code](https://claude.ai/code/session_01VDtqoecgES7ScQYGbFVDRv)_ Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal
pushed a commit
to akarma-synetal/framework
that referenced
this pull request
Oct 7, 2026
…s config (objectstack-ai#21953) Fixes objectstack-ai#21950 Clause-②: no ## What One paragraph in `content/docs/data-modeling/drivers.mdx`: the plugin-contributed-driver paragraph that objectstack-ai#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 objectstack-ai#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 objectstack-ai#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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Refs #21840
Clause-②: no
What
One paragraph in
content/docs/data-modeling/drivers.mdx, after the note that a plugin-contributed driver'sconfigis left unvalidated. It adds that the platform does not guess which keys of thatconfighold credentials, that theconfigis stored and served to administrators as written, and that keeping secrets out of it is the plugin author's responsibility, with the credential going in the bound secret (external.credentialsRef).This is the one change the maintainer kept when #21840 was closed as not planned (ruling on #21921).
Checks
node scripts/check-doc-authoring.mjs: exit 0.🤖 Generated with Claude Code
https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6