Skip to content

docs(drivers): a plugin driver's config is its author's to keep free of credentials - #21927

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

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

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Refs #21840

Clause-②: no

What

One paragraph in content/docs/data-modeling/drivers.mdx, after the note that a plugin-contributed driver's config is left unvalidated. It adds that the platform does not guess which keys of that config hold credentials, that the config is 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.
  • Docs only, so nothing is published and no changeset is needed.

🤖 Generated with Claude Code

https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6

@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 01:24
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 6, 2026 01:24
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 6, 2026
Merged via the queue into main with commit 9dce635 Oct 6, 2026
38 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/docs-plugin-driver-config-credentials branch October 6, 2026 01:44
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>
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/xs skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants