Repository navigation
[security] sys_metadata.metadata is a general cleartext sink: any authored artefact whose schema permits an inline credential lands it there (datasource config.password, connector authentication) #7990
Description
Filed by the domain:services PM seat from the #7902 credential-persistence survey (report: comment 5264546868). Unassigned, no pm:queue, no domain:* — routing is genuinely undecided, see below. For triage to grade and route.
The finding — stated as the class, because it is one
Two of the survey's hits are the same bug wearing two hats. sys_metadata.metadata is a general cleartext sink: any authored artefact whose zod schema permits an inline credential lands that credential, in cleartext, in a column reachable through the ordinary data API (sys_metadata declares apiMethods: ['get','list']).
| # | Artefact type | What lands inline | Evidence | Who reads it back |
|---|---|---|---|---|
| ①-c | type='datasource' |
driver config.password — the driver zod schemas permit an inline password |
postgres.zod.ts:100-105 (also mysql.zod.ts:95, mongo.zod.ts:96, turso.zod.ts:135); persisted whole by datasource-admin-plugin.ts:87 — JSON.stringify(record) |
getDatasource() returns config verbatim — datasource-admin-service.ts:199 |
| ①-d | type='connector' |
Connector.authentication — the runtime shape, permitting inline token / key / password / clientSecret |
connector.zod.ts:723-725; connector-auth.zod.ts:19-51; connector is a metadata type (metadata/src/plugin.ts:95) |
connector provider factories, e.g. mcp-provider.ts:63-80 |
Why the class framing matters for the fix
A per-artefact fix closes two holes. A guard at the metadata write boundary closes the category — including artefact types that do not exist yet. That is the choice a card has to make, and it is why this is filed as one card rather than two:
- Per-artefact (services-side): make each schema refuse the inline form and divert to the existing mechanism. Both already have one — datasource has the secret binder (
datasource-secret-binder.ts, which correctly encrypts tosys_secretand stores an opaque handle), and connectors have ADR-0097'sauth.credentialRef, whose own comment says "deliberately no field to inline a secret here". So in both cases the right answer already exists and the legacy inline path was simply never closed. - Boundary guard (engine-core-side): refuse, at the
sys_metadatawrite, any payload carrying a credential-shaped inline value. Closes the class, but needs a definition of "credential-shaped" that does not produce false refusals on legitimate config.
datasource-admin-service.ts:179 already carries a "credential-stripped" comment. The survey's reverse-check confirmed the comment refers to credentialsRef/cleartext handling and never to config — no scrub of config exists anywhere in service-datasource/src. The code claims a protection it does not perform.
Existing rows
Both hits need a migration story for rows already written, and neither is a pure code fix. Not scoped here.
Explicitly NOT claimed
- No leak is demonstrated. These are reachable-cleartext findings; whether a given deployment's permissions actually expose
sys_metadatato a non-admin persona was not evaluated. - The
service-datasourcebinder itself is CLEAN and must not be confused with ①-c. The survey corrected an earlier (PM-authored) claim that the binder stored a credential in a plain column — it does not: it encrypts intosys_secretand persists only an opaquesys_secret:<id>handle. ①-c is a different bug in the same package: the driverconfigblob bypasses the binder entirely. password-typed fields are out of scope (ADR-0100). NoteField.password()has zero occurrences repo-wide, so this is not that.
Routing note — why no domain:*
The artefact schemas are packages/spec (domain:spec); the datasource/connector write paths are services (domain:services); a write-boundary guard on sys_metadata is packages/metadata* or packages/objectql (domain:metadata / domain:engine-core). Which lane owns it depends on which fix shape is chosen — and choosing the shape is the first thing a card has to do. Flagged rather than guessed, per the anchoring rule.
Evidence
Measured 2026-08-12 by the dev on #7902 (read-only survey, pr: null), file:line evidence as cited. Full 26-surface verdict table, including the reverse-checks validating the clean verdicts, in comment 5264546868 on #7902.
Source
Split out of #7902. Sibling cards from the same survey: #7986 (webhook headers), #7987 (sys_account OAuth tokens).
Activity
hotlong commentedon Aug 12, 2026
Triage:
needs-user-decision, routeddomain:spec, type Bug. This is deliberately NOT auto-adjudicated: both candidate fixes narrow the accept-set of a published authoring surface (protocol change), and existing stored rows need a migration story — both are on the manual floor.Background
Two survey-measured hits are one class:
sys_metadata.metadataaccepts and serves inline credentials for any artefact whose schema permits them (datasource driverconfig.password, connectorauthentication). Both artefact types already own the correct mechanism (datasource secret binder →sys_secrethandle; ADR-0097credentialRef), and the legacy inline path was simply never closed. Full evidence in the card body and #7902 comment 5264546868.Premises (re-checked at triage on
origin/main, with re-check commands)git grep -n "credentialsRef" origin/main -- packages/spec/src/data/driver/postgres.zod.ts(line ~100: "Preferexternal.credentialsRef" on an accepted inline field).git grep -n "deliberately no field" origin/main -- packages/spec/src/shared/connector-auth.zod.ts(line 96).git grep -rn "credential-stripped" origin/main -- packages/services/service-datasource(README/route ledger claim) vsgetDatasource()returningconfigverbatim (datasource-admin-service.ts:199).Question
Close the two measured holes per-artefact at the contract, or build a credential-shape guard at the
sys_metadatawrite boundary?Options
credentialRef. Needs a migration story for rows already written.sys_metadatapayload carrying a credential-shaped inline value. Closes the class including future artefact types, but requires a heuristic definition of "credential-shaped".Recommendation: A (B deferred until a third surface is measured — i.e. effectively C with B parked, reopen trigger = the next measured artefact-type hit)
credentialsRef") is the strongest shape — an AI author cannot land the anti-pattern. A heuristic runtime guard can both false-accept (novel key names) and false-refuse (legitimate config).Consequences if ruled A (recorded for dispatch, not pre-empting the ruling)
packages/specchanges ⇒ dispatch underclaude-fable-5per the 2026-08-12 model clause; contract-first split (spec half first, services write-path/read-path halves asBlocked-by:sub-cards).target:*board) is part of the ruling, not a dev call.本评论来自分诊座位(scheduled session
session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。Generated by Claude Code
hotlong commentedon Aug 12, 2026
Maintainer ruling recorded — Option A adopted. Provenance: maintainer, 2026-08-12 (live, in this PM session), on the consolidated decision digest: 「接受你的全部建议。」 — adopting this card's four-lens recommendation A.
Binding ruling: per-artefact contract closure — the driver config schemas and the connector runtime shape refuse inline credentials at publish, with error guidance diverting to the mechanisms that already exist (datasource secret binder →
sys_secrethandle; ADR-0097credentialRef). The heuristicsys_metadatawrite-boundary guard is parked; reopen trigger = a third measured artefact-type surface.Dispatch constraints (spec seat): contract accept/reject change ⇒
model: claude-fable-5mandatory. Contract-first split: the spec half (schema refusals + guidance) first; the services write/read-path halves (including the false "credential-stripped" claim atdatasource-admin-service.tsand the migration story for existing cleartext rows) asBlocked-by:sub-cards in their lanes. The existing-rows migration is in scope of the programme, not optional. Pin both directions: inline credential refused loudly with guidance naming the ref mechanism; ref-based config still accepted byte-identically.本评论来自分诊座位(scheduled session
session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。Generated by Claude Code
huangyiirene commentedon Aug 12, 2026
Claim: PM loop round 1 (duty-division session, seat post #6017) — spec half only, per the maintainer's Option-A ruling and its contract-first split
Session:
session_0123k4cam2jEAkPmbJeoaY3rBranch:
claude/issue-7990-inline-credential-refusalWorktree:
objectstack-issue-7990Domain:
domain:specFile surface:
packages/spec/src/data/driver/{postgres,mysql,mongo,turso}.zod.ts(+driver/common.zod.tsonly if the refusal factors there),packages/spec/src/shared/connector-auth.zod.ts,packages/spec/src/integration/connector.zod.ts(runtimeauthenticationshape), their tests; regeneratedreferences/**ride. ⛔ NOT in surface:packages/services/**(write/read paths + the false "credential-stripped" claim), existing-rows migration,data/driver/memory.zod.ts/test-typecheck-debt.json/ strictness-ledger audit docs (all held by in-flight #7985). (stop on breach; explain in the report)Container & model: M,
mode:subagent,model: fable(claude-fable-5— mandatory clause: accept/reject change)Serial constraints cleared: #7985 (in flight) touches
data/driver/memory.zod.ts+ debt ledger only — file-disjoint from this surface; #7892/#7972 areui/faces. Downstream services/migration halves will be filed asBlocked-by:sub-cards in their lanes at ACCEPT time (this comment is the provenance).Generated by Claude Code
feat(spec)!: refuse inline credentials at publish — driver config + c…
14 remaining items
os-project-manager commentedon Aug 16, 2026
Unlock scan: all three carriers are closed with merged PRs — released,
pm:blocked⇒pm:queue. ⛔ The remaining work is the spec seat's final programme verification and closure decision, NOT a dev dispatch. Triage seat (whole-repo Routine), scheduled fire ~01:08Z 2026-08-16.Corrected readings (direct GETs this round — the re-block comment above carried stale state for two of the three)
/metaread-path redaction + write-path carry-forward)The 2026-08-15 20:06Z re-block described #8155 and #8152 as "open and in flight"; both had already closed before that comment was written (the same stale-table trap that comment itself corrected for a different row — recording it plainly, not as blame: the table it inherited was stale, the discharge is what matters).
What this card now needs — quoting the re-blocking seat's own instruction
That condition is met. Returning it to the queue so the
domain:speclane's next pass performs the final programme verification: both original hits' write doors closed (spec refusals), read paths redacted (#8126 + #8154's hook), migration story landed (#8155), turso slot restored (#8152) — then closes this anchor card. Known non-blocking residue already carried elsewhere: the URL-embedded-credentials decision card and the census finding toward the parked Option-B reopen trigger.⛔ Do not dispatch a dev against this card as if it were a fresh work item — the verification pass is a PM read, and dispatching would re-do work that lives in the named carriers.
Generated by Claude Code
os-project-manager commentedon Aug 16, 2026
Closed as completed — final programme verification executed by symptom on
origin/main(@510a4bd+), per this card's own closure protocol ("the unlock sweep returns this card for final programme verification and closure"). Spec seat, sessionsession_01Fgvh1iEJfxetei7aNVdtJt.Correction on the record first: my 2026-08-15T20:03 re-block of this card cited #8155 as open — that row was inherited from #8081's closure table without direct verification, and it was stale (#8155 closed 08-14 via merged PR #8699). The unlock sweep's release back to
pm:queuewas correct; the re-block was my error, same failure mode as the #8153 row I did catch.The programme, verified surface by surface (all on
origin/main, by symptom)authentication)refusedInlineCredentialKey/INLINE_CREDENTIAL_REFUSEDpresent incommon.zod.tsurlCredentialQueryParamspresentredactDatasourceConfigwired indatasource-admin-service.tsmetadata-redaction.tspresent inmetadata-protocoldatasource-credential-migration.ts+ suites presentThe card's class question (per-artefact vs boundary guard) was ruled Option A (per-artefact) by the maintainer earlier in the programme and has now been executed to completion across both hits and all carriers.
pm:queueremoved with the closure per the state-machine hygiene precedent.Residue explicitly NOT lost: #8873 (postgres arm, broken one layer lower — its own card, its own lane) and #8696/#8876 (mongo arm username accessor — dispatched this hour) are successor work in their own right, not conditions on this programme.
Generated by Claude Code
feat(spec)!: refuse inline credentials at publish — driver config + c…
fix(service-datasource): stop serving stored credentials on the datas…
fix(service-datasource): read a turso datasource's bound secret into …
feat(spec)!: refuse URL-embedded credentials (user:password@host) in …
feat(spec): refuse `${…}` placeholder syntax in connection-material d…
feat(spec): govern connector + analytics_cube in the liveness ledger …
docs(changeset): correct two false mechanism claims in a pending conn…
test(spec): security, ai, identity, integration, migrations, marketpl…
test(spec): the last data/ file group's test titles state each cited …