Skip to content

fix(spec)!: credential-shaped datasource config values are refused at write and redacted on every read door for drivers with no shipped contract - #21877

Closed
objectstack-fleet[bot] wants to merge 19 commits into
mainfrom
claude/issue-21840-datasource-secret-keys
Closed

objectstack-fleet[bot] wants to merge 19 commits into
mainfrom
claude/issue-21840-datasource-secret-keys

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #21840

Clause-②: yes (narrowing)

What changed

Credential material in the config of a datasource whose driver the platform ships no config contract for (a plugin-contributed driver such as com.vendor.warehouse) is now refused at publish and withheld on every read door. A driver with a shipped contract (postgres, mysql, mongodb, turso, sqlite, sqlite-wasm, memory and their aliases) is judged exactly as before, at both doors.

  • One walk, two doors (packages/spec/src/data/driver/contractless-credentials.ts): findContractlessCredentials walks a contractless driver's config and reports every credential position, with its config.<path>. The write door refuses each finding and the read door withholds each finding, so the two doors cannot disagree. isContractlessDriver decides which drivers it applies to: those with no registered config schema.
  • The key judgment (isCredentialShapedConfigKey): a key is NFKC-normalised, then judged on its whole name, split into words at separators and camel-case boundaries, case-insensitively. It matches whole words, never substrings. A key longer than 256 characters is credential-shaped without being read. A key whose last word is a locator, identifier or descriptor (credentialsRef, accessKeyId, tokenUrl, passwordFile, secretsManagerRegion and the like) is never credential-shaped, nor is a multi-word key that starts with a flag or count word (useDefaultCredentials, maxTokens). Otherwise it is credential-shaped when a word is password, passwd, passphrase, secret or credential, when its last word is a token-type stem (token, pwd, jwt, cookie, auth, bearer, apikey, pfx, pkcs12, p12 and others), or when it names key material (apiKey, privateKey, sslKey, serviceAccountKey and others). A bare accessKey, primaryKey and partitionKey stay accepted. A one-word key with no boundary left (APIKEY, dbpassword) is judged on its folded spelling by the same rules. The changeset has the full word lists.
  • Non-ASCII keys: a key that holds non-ASCII text is credential-shaped only when that text sits inside or next to a credential word: it contains a credential word of another script (密码, パスワード, пароль and others in the changeset); it reads as credential-shaped once invisible format characters are removed and Cyrillic and Greek look-alike letters are read as Latin; an ASCII credential word touches a non-ASCII character (password密码); or a credential word of four or more letters has at most one non-ASCII stand-in or insertion per four letters (tok€n). Otherwise a run of non-ASCII characters is a word of its own, so 客户名称, Größe and café are accepted. The same rule judges query, form and connection-string segment parameter names.
  • A bare key (or keys) in an object is credential material only in these places: inside a credential-shaped holder; directly in a header-ish holder's map (headers: { key } is the header named key), but not in an element of a list under it, where key is a pair's label (headers: [{ key: 'Authorization', value }]); inside a TLS holder (a key with a word ssl, tls, mtls, x509, pfx, pkcs12, or a word starting with cert: ssl: { key, cert, ca }); or when its value looks like key material. Key material here means a secret-looking string (looksLikeSecretValue), 16 or more characters of hexadecimal holding a letter, digit-free base64 whose upper and lower case alternate like random text, or bytes. Camel-case names and paths stay names, so { key: 'email' } and { key: 'customerEmailAddress' } are accepted.
  • Write door (packages/spec/src/data/datasource.zod.ts): DatasourceSchema adds one custom issue per finding, at the value's own config.<path> (array elements included, such as config.servers.0.password), naming the remedy: remove the inline credential and bind it as the datasource's secret (the connection form's secret field, or external.credentialsRef). Every door that parses the schema is covered: defineStack({ datasources }), PUT /api/v1/meta/datasource/:name, and Setup → Datasources create and update. The connection test answers ok: false. What is refused:
    • a non-empty string, a number, non-empty bytes, or an array of these under a credential-shaped key;
    • the secret leaves of a credential-shaped object (credentials: {…}, auth: {…}), except leaves whose last word is a descriptor or an identity (user, clientId, host, scope and the like), so credentials: { type, clientId } is accepted whole. The exemption covers leaves only; an object below such a key is still judged as credential context;
    • the value of a pair object whose label (name, key, header or headerName) is credential-shaped, and the value of a [name, value] tuple or a flat raw-headers list whose name is credential-shaped (an Authorization, X-API-Key or Cookie header);
    • a credential inside a string anywhere: a URL userinfo password, or a userinfo username that looks like a secret; a credential URL query, fragment or form parameter, or a ;key=value tail property; the Oracle thin-driver userinfo; a credential segment of a semicolon-delimited connection string (quoted values honoured); a credential keyword of a libpq keyword/value string; a scheme-less user:password@host userinfo; a JSON-encoded object or array, walked by the same rules; PEM private-key armour (-----BEGIN … PRIVATE KEY-----, including RSA, EC, DSA, ENCRYPTED, OPENSSH and PGP PRIVATE KEY BLOCK), in a string or in bytes; and a Name: value header line with a credential-shaped name. A header line is read on any line of a multi-line string, but a single-line string is read as a header line only under a header-ish key, so a one-line description: 'Password: …' is prose;
    • what is not read as a credential inside a string: a libpq or segment value that starts with a SQL bind placeholder ($1, ?, :name), as in WHERE token = $1; the part after an opaque URI scheme (mailto:, sip:, sips:, tel:, urn:, xmpp:, news:, im:, pres:), which is read on its own instead of as user:password@host; and a time of day before an @ (12:30@);
    • a string or bytes longer than 65,536 characters or bytes (MAX_JUDGED_STRING_LENGTH), which is judged credential material without being read;
    • a subtree nested deeper than 16 levels (CONTRACTLESS_CREDENTIAL_WALK_DEPTH), and a Map or Set anywhere, which cannot be judged and are not accepted unjudged.
  • Still accepted at the write door: an empty string (the explicit way to clear a stored value), a boolean, a value made only of upper-case environment placeholders (${API_KEY}), plain array data with no credential-shaped key, and every other key. The config shape itself stays unjudged.
  • Read door (packages/spec/src/data/datasource-credential-redaction.ts): the one redactor, redactDatasourceConfig, now withholds every position the same walk reports for a contractless driver. A credential value is dropped (inside an array element too, without shifting its siblings), a credential embedded in a string is removed from it (redactEmbeddedCredentials), and a PEM private-key block is removed from its string. The served projection is then judged again, and redacted again, until it holds no finding, so an untouched Save of what was served passes the write door. A projection that has not settled after 8 passes is served empty. Every read exit already routes through this redactor: /api/v1/meta/datasource (item, list, /published, /layers, history), /api/v1/datasources (item and list), the data door over sys_metadata / sys_metadata_history, and the audit ledger's and activity feed's copies. No second redaction helper is added. Audit copies written before this release are projected through the same redactor by os migrate audit-metadata-bodies --apply.
  • Edit round trip (packages/services/service-datasource/src/datasource-config-redaction.ts): restoreRedactedConfig resolves each array hop by identity: the same index in an array left as served, otherwise the one element equal to the served projection, unique on both sides. A value whose element changed, is gone or is ambiguous is dropped, never carried onto another element. It then redacts the grafted config again and keeps a graft only where the read path would still withhold it, repeating until nothing more drops. So a pair's value beside a label renamed to a non-credential name, a deleted label, or a renamed key: label is dropped. An untouched Save, where the patch equals the served projection, skips this second walk.
  • /meta carry-forward (packages/metadata-protocol/src/metadata-redaction.ts): the PUT /api/v1/meta/datasource/:name carry-forward applies the same identity rule to id-less array elements and nested arrays, which it used to skip, so an untouched GET-then-PUT of a legacy row no longer drops values.
  • Migration planner (packages/services/service-datasource/src/datasource-credential-migration.ts): for a contractless row, every top-level key that holds a finding of the same walk is reported as residue and refused with the remedy, instead of nothing-to-migrate.
  • New @objectstack/spec/data exports: isCredentialShapedConfigKey, embeddedCredentialOf, redactEmbeddedCredentials, connectionStringCredentialKeys, findContractlessCredentials (with ContractlessCredentialFinding, whose embedded findings under a header-ish key carry headerish: true, and CONTRACTLESS_CREDENTIAL_WALK_DEPTH), withholdContractlessCredentials, looksLikeSecretValue, MAX_JUDGED_STRING_LENGTH, the EmbeddedCredentialOptions type (its headerish option is taken by embeddedCredentialOf and redactEmbeddedCredentials), and isContractlessDriver. api-surface/ and export-origins/ regenerated.
  • ADR anchor scripts/adr-anchors/packages__spec__src__data__datasource.zod.ts.json (ADR-0015, ADR-0062). The changeset .changeset/21840-contractless-datasource-credentials.md states the breaking change, the full refused and accepted lists, and the remedy.

Why refuse at write rather than route into the secret store

ADR-0015 section 10: credentials never appear in metadata artefacts. ADR-0062 D3: the one route a credential has into a driver is the bound secret (external.credentialsRef), resolved at connect and handed to the driver factory. For a contractless driver nothing says which key its factory reads its credential from, so silently moving config.apiKey into the single bound secret would hand the factory a secret it may never read and strip the key it does. That is the same reason the credential-migration planner already refuses to re-home such a row. Refusing is loud and names the working remedy; known drivers already refuse inline credentials the same way.

Tests

Rounds 3 and 4 are described in the os-dev-reports on #21840. Round 3 (head 2320178c9d, comment 6002380150) added a linear key split with a 256-character cap on judged names, identity-based carry-forward, a lenient one-pass libpq scan, more header shapes and key spellings, embedded credentials in URL usernames, signed-URL signatures, JSON-, form- and fragment-encoded strings, leaf-only descriptor exemption, byte and Map/Set values, and a bare key judged only in context or by its value. Round 4 (head 5c405846a5, comment 6004802884) added the re-judgment fixed point in the edit round trip, PEM private-key armour and TLS holders, the non-ASCII key rule, the header-line, SQL-placeholder, opaque-scheme and time-of-day readings, the 64 KiB unread cap, hex and base64 bare-key values, and a served projection judged again until clean. The figures below are from round 4.

All runs are against the tree pushed as HEAD 5c405846a5. The tests import the subject from src.

  • packages/spec/src/data/datasource-contractless-credentials.test.ts: 434 tests, covering the key judgment in both directions, embedded credentials in strings, the write door and the read door per shape, and known-driver behaviour unchanged. Round-4 cases in both directions: PEM per armour type, TLS holders, pfx, the accepted and refused non-ASCII sets, parameter names, prose, SQL, mailto, sip, tel, urn and time-of-day values accepted with controls still refused, the 64 KiB cap with an at-cap control, hex and base64 bare-key values with name controls, bounded-time cases, and cases proving the served projection is accepted by the write door. Whole spec project: 619 files / 18884 tests passed (1 todo).
  • packages/services/service-datasource/src/__tests__/datasource-contractless-credentials.test.ts: create and update refused per shape with nothing persisted and no secret minted; getDatasource and listDatasources withhold a legacy row's credentials; the untouched round trip carries them forward; the planner names the residue. Six new carry-forward tests: label renamed, label deleted, key: label renamed, renamed to another credential name (keeps the value), the untouched-Save control, and a re-read through the service. Whole package: 40 files / 762 tests passed.
  • packages/qa/dogfood/test/datasource-contractless-credentials.dogfood.test.ts: the showcase composition with the admin routes and the audit writer mounted as os serve mounts them, across a cold boot on one database file. Both write doors refuse and nothing is stored; the clean datasource saves; a row seeded at rest with the pre-refusal body is served by every read door without its credentials, with a positive-control marker present; the audit ledger's copy carries none. New case 4 drives the admin PATCH edit door on a seeded legacy row with renamed labels: the stored row and two read doors carry no withheld value, and the positive control (the edit reached the stored row) holds. Whole dogfood package in 4 batches: 204 files passed, 1 skipped (477 + 401 + 384 + 317 tests passed).
  • Consumer suites: metadata-protocol 217 files passed, 3 skipped / 27925 tests passed, 19 skipped. plugin-audit 39 files / 630 tests passed.
  • Ablations on the committed tree through scripts/ablation-replace.mjs, each anchor verified 1 to 0 on disk, each restore proven by blob equal to HEAD and an empty git diff HEAD: (1) making the edit round trip keep every graft turned 4 service tests red (label renamed, label deleted, key: renamed, through the service), while the control and the renamed-to-credential case stayed green; (2) dropping the TLS-holder clause turned 1 test red (a PEM under ssl.key is still caught by the armour reading); (3) removing PEM detection turned 7 tests red. The restore runs re-ran 434/434 and 30/30 green. Two earlier attempts that never measured anything (one refused by the tool, one mangled by shell quoting) were discarded.
  • Typecheck green: @objectstack/spec (including scripts and the test layer), @objectstack/service-datasource, metadata-protocol, plugin-audit, dogfood.
  • Gates: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran: 99 derived, 98 run with exit 0, 1 NOT MEASURED. check:generated green after regenerating api-surface/ and export-origins/. check:type-check-debt timed out at the 300 s per-gate budget and passed on a rerun (79/80 packages, debt unchanged).

NOT MEASURED: check:dual-build-cjs-loads, reason: PREREQUISITE NOT MET (exit 3; it needs a full repo build). Declared to CI.

The branch was at least 3 commits behind origin/main when the gates ran; main was not merged in this round. This diff touches no fake engine (scripts/engine-double-contract.pinned.json changed on main).

Acceptance notes

  • The out-of-repo consumer population is not measured. No datasource in this repository uses a contractless driver; a plugin driver elsewhere that authored inline credentials sees the refusal on its next publish (the changeset states the breaking change and the remedy).
  • A plugin driver that needs more than one secret has no remedy for the second one today: the datasource secret binder fills exactly one slot. That is the existing one-slot limit, not changed here.
  • On a legacy contractless row that still holds a credential, an admin edit that changes config carries the withheld value forward and is then refused; sending that key as an empty string clears it. A metadata-door save of the served body is not affected (its gate runs before the carry-forward).
  • An inline value longer than 64 KiB under a contractless driver, such as a large CA bundle, is now refused at publish and served empty; reference such material by path instead.
  • Open decision: the placeholder exemption accepts only upper-case environment names (${API_KEY}); a lower-case ${…} is judged as written. The platform's own environment names are upper-case.
  • Known over-refusals, not leaks: a scheme-less user:x@host-shaped value that is not a credential is refused; a key such as secretsManager holding a provider name is judged credential-shaped; and credential words of other scripts match as substrings inside a key.
  • Known over-acceptance at the edge: a libpq or segment value that starts with :word is read as a SQL bind placeholder, not a credential.
  • A key-labelled pair placed directly under a header-ish key is served without its key label, because there key is read as the header named key.

Generated by Claude Code

claude added 8 commits October 5, 2026 11:39
…edential exports

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-Authored-By: Claude <noreply@anthropic.com>
…/0062 on the write door

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/metadata-protocol, @objectstack/service-datasource, @objectstack/spec, touching 136 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/src/data/driver/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/automation/connectors.mdx (via headerName (literal, a string literal in PAIR_LABEL_KEYS))
What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/export-origins/data.json, packages/spec/src/data/driver/index.ts) — pages documenting those are invisible to this run
  • 11 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 139 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 cab639671528ef6f3a201e8995794378a4a28bfe → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 cab639671528ef6f3a201e8995794378a4a28bfe → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6a4fb54fcaf8ce62fb4c7f8ae80796e2e041dc8b
Local-runs: none

Inputs read: card #21840 body and all four comments (triage 5990389094, claim 5990748393, os-dev-report 5995346311, claim correction 5995372304); PR #21877 body, its 11-file list, and the net diff origin/main...6a4fb54fca; the 36 check-runs on this head. Nothing was built, run or re-run.

① Derived judgments

  1. DatasourceSchema accept set narrows, for contractless drivers only. Named RIGHT. Before this change, reportDriverConfigIssues returned early on known: false. It now calls reportContractlessInlineCredentials, which refuses three things, each at its own config path: a non-empty string under a credential-shaped key or under a credential-shaped enclosing object; a URL userinfo password or a credential query parameter; and a credential segment of a connection string. These stay accepted: empty strings, ${…} placeholders, non-string values, arrays, and every other key. The known: true branch is byte-for-byte unchanged, so a driver with a contract is judged exactly as before. The changeset states the predicate rule exactly as the code implements it (fragments, endings, locator-ending exclusions). Consumers therefore get an accurate statement of what is now refused.

  2. The read-door redactor redactDatasourceConfig now withholds more, for contractless drivers. Named RIGHT. This is an output narrowing, not an accept-set change. isHidden and redactString add the same predicate at every depth, plus connection-string segment stripping. The work happens inside the one existing redactor, so the triage's "no second redaction helper" condition holds. Read and write use the same predicate. The read side drops a credential-shaped subtree whole, and the write side refuses strings beneath one, so the two doors agree. The read side is stricter on non-string values, which is the safe direction.

  3. Published surface widens. Named RIGHT. @objectstack/spec/data is the ./data exports entry, and src/data/index.ts re-exports driver/index and datasource-credential-redaction. The new exports are isCredentialShapedConfigKey, connectionStringCredentialKeys, redactConnectionStringCredentials and isContractlessDriver. The api-surface/data.json and export-origins/data.json shards are regenerated to match: 4 additions, nothing removed or renamed.

  4. @objectstack/service-datasource planner behaviour. Named RIGHT. unbindableCredentialKeys now reports a contractless row's credential-shaped keys and credential-bearing connection strings as residue, instead of returning nothing-to-migrate. The function is internal, and the package's exports are untouched.

  5. Residual precision of the name predicate. Recorded, not blocking. The predicate is a closed heuristic: enumerated key-material endings and an enumerated locator/descriptor exclusion list. It errs in both directions:

    • Over-inclusive. Some descriptor keys contain a fragment but end in a word that is not on the exclusion list, for example a provider, source, method, policy or region selector. These are refused at write and dropped on read, and the bound-secret remedy does not fit them.
    • Under-inclusive. Some abbreviated credential spellings, and some key-material names that carry a value-format suffix, match neither a fragment nor an ending. They are neither refused nor withheld for contractless drivers, which is the state before this PR.

    Neither error mis-states the contract: the changeset publishes the exact rule. The PR is a strict improvement on main for the card's class, and the card's withheld positions are named in the changeset title and are covered. isCredentialShapedConfigKey is now public, so widening it later is itself a further accept-set narrowing. That later change will need its own Clause-②: yes (narrowing) changeset. The specific spellings are passed to the PM seat privately (RUNNER rule 2), and a follow-up card is recommended.

② Semver level

Clause-②: yes (narrowing)

  • @objectstack/spec: minor. This matches what the diff publishes: one accept-set narrowing (BREAKING) and four additive exports. The changeset carries the BREAKING banner and the Clause-②: yes (narrowing) line. It carries exactly one ADR-0087 marker, not-required (no-migration-prescription). That disposition is sound: no key, export or stored shape is retired or renamed, and re-homing a credential into the bound secret is an operator act, not a mechanical FROM to TO rewrite. The remedy is stated for authors (remove it from config, bind it via the connection form's secret field or external.credentialsRef). The title fix(spec)!: agrees. yes with at least minor satisfies AGENTS.md directive 3.
  • @objectstack/service-datasource: patch. Behaviour fix only, no export change. Correct.
  • @objectstack/dogfood and scripts/adr-anchors: tests and tooling only, no published surface.
  • The claim originally said Clause-②: no. Comment 5995372304 corrected it to yes (narrowing), and the PR body, the changeset and this record agree.

③ Boundary flags

  • open_questions[0], the Clause-② line. Answered. Option A, keep yes (narrowing), is correct per ② and was already adopted by the claim correction 5995372304.

  • open_questions[1], a plugin driver needing more than one secret. Answered with option A: accept it as the existing one-slot binder limit, and record it. No contractless driver exists in this repository, and the limit predates this change. Escalate to a decision card (option B) only if a real multi-secret plugin driver is named.

  • out_of_scope_findings[0], the verify harness not mounting the admin routes or the audit writer. Observation only. The dogfood file mounts both the way os serve does. No carrier is needed.

  • out_of_scope_findings[1], the legacy contractless row edit-then-refuse path. Disclosed in the PR's Acceptance notes and the changeset. Sending the key as an empty string clears it, so a remedy exists. Accepted.

  • Out-of-repo consumer population NOT MEASURED. Declared in both the changeset and the PR body. That is acceptable for a launch-window minor narrowing whose changeset names the remedy.

  • Dev NOT MEASURED items. check:dual-build-cjs-loads and the packages/spec/scripts/** tests were declared to CI. They are covered by the head's Build Core, Type Check and Test Core jobs, which are still pending (below).

  • Governed surface. The file list touches no governed path (docs/adr/**, skills/**, AGENTS.md, CLAUDE.md, .claude/**), so this record is not a landing key. Landing still waits on every check going green.

  • Check-runs on this head at review time.

    • Green: Type Check · source gates, Spec property liveness, Check Changeset (one run), Governed Surface Queue Guard, claim/single-writer guards, Check PR Size, Check Documentation Links, Flag docs affected, Auto Label, filter.
    • Skipped: Console Pin Gate, Build Docs, Packed-tarball smoke.
    • Still pending: Build Core, Lint & Repo Gates, Check Changeset (second run), Type Check · workspace / debt ledger / consumer gates, Test Core 1–6/6, Dogfood Regression Gate 1–3/3, Dogfood Verify CLI, Temporal Conformance.

    This verdict judges the contract. It does not stand in for any pending gate.

Implemented-by: claude/issue-21840-datasource-secret-keys
Reviewed-by: session_018zT8d8NpiQ1ExhuNd5TxY6

VERDICT: PASS

claude added 4 commits October 5, 2026 13:51
…airs, more spellings and connection-string forms

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-Authored-By: Claude <noreply@anthropic.com>
…dentials holding ; = : @, libpq ; values, header tuples; whole-word one-word keys

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
claude added 2 commits October 5, 2026 18:05
…y identity

Round-3 review fixes for the contractless-driver credential walk:

- the key judgment is linear (the capital-run split looks ahead instead of
  capturing the run) and bounded: a key, segment key, parameter or header
  name longer than 256 characters after NFKC, or holding a non-ASCII
  character, is judged credential-shaped unread;
- libpq pairs are found leniently, one pass;
- header shapes: Name: value lines, raw-headers lists, a tuple under a
  header-ish key, tuples longer than two, every pair label;
- key names: ssl/tls key material, privkey, the data/content/hex/string/
  str/raw/hash qualifiers, basicauth;
- embedded shapes: token-shaped URL usernames, sig and X-Amz-Signature
  parameters, JSON-encoded strings, form-encoded strings, URL fragments;
- a descriptor key exempts leaves only; bytes are one value; Map and Set
  are judged whole; a bare key needs a secret-looking value or a
  credential-shaped or header-ish holder;
- restoreRedactedConfig and the /meta carry-forward resolve every array
  hop by identity (unchanged array, else a unique served projection) and
  drop a withheld value whose element changed, is gone or is ambiguous;
  the /meta path now follows id-less elements and nested arrays.

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
claude added 3 commits October 5, 2026 21:14
…e read path still withholds it

restoreRedactedConfig re-redacts the merged config and keeps only the
grafts whose landing path is still withheld, repeating until nothing more
drops (the /meta carry-forward's loop). An untouched Save skips the second
walk: the merged config is the stored one.

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
… keys by words; prose and SQL stay accepted

- PEM private-key armour is secret material in any string (and in bytes);
  a bare `key` under an ssl / tls / cert* holder is key material; `pfx`,
  `pkcs12` and `p12` are credential words.
- A non-ASCII key is credential-shaped only when its non-ASCII text sits
  inside or next to a credential word (confusable letters, format
  characters, stand-ins, adjacency, credential words of other scripts);
  otherwise a non-ASCII run is a word of its own.
- A single-line `Name: value` string is read as a header line only under a
  header-ish key; SQL bind placeholders are not libpq or segment values;
  opaque URI schemes and times of day are not scheme-less userinfo.
- A string or bytes longer than 64 KiB is judged credential material unread.
- A bare `key` also counts hex and digit-free base64 key material.

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
claude added 2 commits October 5, 2026 21:23
… serves holds no finding

A bare `key` under a header-ish holder is the header named `key` only in a
header map, not in a list element (a pair's label). The contractless read
projection is judged again until it holds no finding, so an untouched Save
of what was served passes the write door. The dogfood edit-door case and
the service tests use a plain-keyed `key:` pair.

Claude-Session: https://claude.ai/code/session_018zT8d8NpiQ1ExhuNd5TxY6
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Closing without merging, per the maintainer's ruling on #21921 (comment 6007092527 and the correction after it). The platform does not guess which values in a plugin driver's config are credentials, and no registration path is added. #21840 is closed as not planned. A one-sentence docs note lands separately.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants