You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
finding: survey — which other config objects still keep credentials in a JSON blob column, now that resolveSecretField() makes the Field.secret() channel readable #7902
Observation-class finding, filed unassigned with no pm:queue — for triage to grade and route. Offered by the dev at the end of #7799 / PR #7901 ("the issue's 'worth checking in the same pass' line is a real second surface, but it is a survey, not this defect — happy to file it if you want it tracked"); the services PM took the offer rather than let it evaporate with the session.
Why this is worth a card now, and was not before
Until PR #7901, the Field.secret() channel was effectively write-only. Measured on that PR:
maskSecretFields (packages/objectql/src/engine.ts:4744) replaces the stored ref with SECRET_MASK on everyfind/findOne, unconditionally and after hooks, with no isSystem carve-out.
So resolveSecret's own docstring — "privileged consumers use resolveSecret() against the stored ref" — described a path no consumer could enter, because there was no supported way to obtain the ref.
The observable consequence: subsystems that needed a credential back out of storage did not use the field type. packages/services/service-datasource/src/datasource-secret-binder.ts keeps its handle in a plain unmasked column; plugin-webhooks kept the HMAC secret inside sys_webhook.definition_json as cleartext ([security] The webhook signing secret is stored in cleartext in sys_webhook.definition_json #7799).
PR #7901 adds ObjectQL.resolveSecretField(object, recordId, field) — a type-guarded read that refuses any field not declared type: 'secret'. That removes the reason those workarounds existed. So the question "who else is storing a credential somewhere a normal read can reach?" changes from "nothing much can be done about it" to "each one now has an available plugin-local fix."
What the survey should cover
For every system/config object that persists third-party or outbound credentials, determine whether the credential is:
Connector credential storage under packages/connectors/* and the connector registry.
Anything else persisting an outbound auth material: plugin-email transport credentials, service-sms provider keys, service-storage backend credentials, embedder-openai / knowledge-ragflow API keys.
The deliverable of the survey is a list with a verdict per object, not a fix — each hit that turns out to be shape ① or ② is its own card, sized on its own migration story (existing rows, fail-closed behaviour with no CryptoProvider, and whether the consumer can tolerate a decrypt at read time).
password-typed fields are out of scope and must stay that way: they are plaintext at rest by ADR-0100, and resolveSecretField's type guard deliberately refuses them so it cannot become a mask bypass. A survey hit on a password field is a different (ADR-level) conversation.
Routing note
The survey spans several lanes (domain:services owns most of the plugin/connector landing sites; domain:engine-core owns the objectql channel; domain:cli/repo:cloud may own host-side credential wiring), so this is a natural split-into-per-lane-findings card once graded — per the services seat's standing discipline ⑫. Triage's call on whether to grade it as one survey card or fan it out immediately.
Evidence
Measured 2026-08-12 by the dev on PR #7901, reading packages/objectql/src/secret-fields.ts, packages/objectql/src/engine.ts:4640-4838, packages/services/service-datasource/src/datasource-secret-binder.ts, and packages/cli/src/commands/serve.ts:2691-2712. Full measurement in the <!-- os-dev-report --> comment on #7799.
Source
Offered by the dev on #7799 (PR #7901) as an explicitly out-of-scope second surface.
Observation-class finding, filed unassigned with no
pm:queue— for triage to grade and route. Offered by the dev at the end of #7799 / PR #7901 ("the issue's 'worth checking in the same pass' line is a real second surface, but it is a survey, not this defect — happy to file it if you want it tracked"); the services PM took the offer rather than let it evaporate with the session.Why this is worth a card now, and was not before
Until PR #7901, the
Field.secret()channel was effectively write-only. Measured on that PR:maskSecretFields(packages/objectql/src/engine.ts:4744) replaces the stored ref withSECRET_MASKon everyfind/findOne, unconditionally and after hooks, with noisSystemcarve-out.resolveSecret's own docstring — "privileged consumers useresolveSecret()against the stored ref" — described a path no consumer could enter, because there was no supported way to obtain the ref.packages/services/service-datasource/src/datasource-secret-binder.tskeeps its handle in a plain unmasked column;plugin-webhookskept the HMAC secret insidesys_webhook.definition_jsonas cleartext ([security] The webhook signing secret is stored in cleartext insys_webhook.definition_json#7799).PR #7901 adds
ObjectQL.resolveSecretField(object, recordId, field)— a type-guarded read that refuses any field not declaredtype: 'secret'. That removes the reason those workarounds existed. So the question "who else is storing a credential somewhere a normal read can reach?" changes from "nothing much can be done about it" to "each one now has an available plugin-local fix."What the survey should cover
For every system/config object that persists third-party or outbound credentials, determine whether the credential is:
sys_webhook.definition_json#7799 shape — recoverable through the ordinary data API by anyone who can read the row), orField.secret()(encrypted intosys_secret, masked on read).Named starting points, from #7799's measurement — not a claim that these are all of them, and not a claim that any of them is currently broken:
packages/services/service-datasource— the binder's plain ref column (deliberate, pre-resolveSecretField; worth re-deciding now).packages/connectors/*and the connector registry.plugin-emailtransport credentials,service-smsprovider keys,service-storagebackend credentials,embedder-openai/knowledge-ragflowAPI keys.The deliverable of the survey is a list with a verdict per object, not a fix — each hit that turns out to be shape ① or ② is its own card, sized on its own migration story (existing rows, fail-closed behaviour with no
CryptoProvider, and whether the consumer can tolerate a decrypt at read time).Explicitly NOT claimed
sys_webhook.definition_json#7799 is the one measured instance and it is fixed by PR fix(webhooks): stop storing the subscriber signing secret in cleartext (#7799) #7901. Everything above is a hypothesis about siblings, filed so the class gets checked once rather than rediscovered one incident at a time.password-typed fields are out of scope and must stay that way: they are plaintext at rest by ADR-0100, andresolveSecretField's type guard deliberately refuses them so it cannot become a mask bypass. A survey hit on apasswordfield is a different (ADR-level) conversation.Routing note
The survey spans several lanes (
domain:servicesowns most of the plugin/connector landing sites;domain:engine-coreowns theobjectqlchannel;domain:cli/repo:cloudmay own host-side credential wiring), so this is a natural split-into-per-lane-findings card once graded — per the services seat's standing discipline ⑫. Triage's call on whether to grade it as one survey card or fan it out immediately.Evidence
Measured 2026-08-12 by the dev on PR #7901, reading
packages/objectql/src/secret-fields.ts,packages/objectql/src/engine.ts:4640-4838,packages/services/service-datasource/src/datasource-secret-binder.ts, andpackages/cli/src/commands/serve.ts:2691-2712. Full measurement in the<!-- os-dev-report -->comment on #7799.Source
Offered by the dev on #7799 (PR #7901) as an explicitly out-of-scope second surface.