Relationships
#3626 typed-secrets 3.3: sensitive-field accessors, data-origin sentinels and reference validation
Opened by zack · 10/19/2026· Shipped 10/20/2026
Outcome
With the internal typed-secrets option enabled, expression-facing data and resource accessors return tagged sensitive fields as production Secret references. Reading a record into CEL does not read its vault values. When the result leaves CEL, the existing boundary conversion issues data-origin sentinels into the run's registry so the value can be loaded at delivery.
This is Phase 3.3 of design/enablers/typed-secrets.md, Sections 3.2, 3.4, 4.3 item 5, 6.1 and 11. It follows Phase 3.2, swamp-club#3625 and builds on the production Secret, fault context and boundary from Phase 3.1, #3619.
Scope
- One structured-record adapter. Interpret
_swamp.sensitiveFields, either"*"or a list of dot-paths, on trusted records. Wrap tagged stored references as production Secrets marked data origin. Coverdata.latest,data.version,findBySpec,findByTagandmodel.<name>.resource, including ordinary set reads and own-namespace, wildcard and cross-namespace forms. Replace expression-facing plaintext resolution on the typed path. The synchronous resource views must remain synchronous. - Strict reference validation. An absent tag means no sensitive fields. A tagged absent or null value stays absent or null. A tagged non-null value must be an anchored stored vault reference of the form
${{ vault.get('v', 'k') }}. A malformed tag, unsafe path or invalid tagged reference fails with the stable, value-free codesecret_reference_invalidbefore the record enters CEL. Reference-looking text outside a tagged path remains ordinary text. Do not add a resource-schema fallback to expression accessors. - Authorization without a provider call. Record the references in each returned record and authorize each under the run's
RunVaultAccessafter evaluation, before the result is used. Preserve the existing access-denial behavior for all tagged fields in the returned record, including fields the expression does not select. This is authorization, not thevault.getexistence check: do not callhas, probe or load here. A missing key or unavailable vault fails only if the reference is delivered, at load. - Lazy views and the container rule. Build new objects without mutating cached records, contexts or live outputs. For JSON resources,
.contentand.attributesrefer to the same adapted object. Secret-valued property/element getters record a Secret only when CEL reads it; selecting a different field records nothing. Preserve lazy resource getters. Boundary release happens after recording stops, so merely returning a record beside an unrelated container comparison does not create a new refusal. - Data-origin boundary conversion. Complete the Phase 3.1 boundary's support for a data-origin reference that has no existing sentinel. Issue its sentinel into the run's
SecretBagRegistry, preserve the data-origin flag through compositions and Optional/container results, and support rebinding it as a Secret. No Secret or adapter view escapes into domain contexts, persistence or astructuredClone. - Layering and rollout. Keep Secret creation and adaptation inside the CEL infrastructure boundary, with the necessary plain-data contracts at domain seams. The relevant existing accessors live in
model_resolver.tsanddata_record_mapper.ts. Update the architecture fitness rules for the adapter without introducing domain imports of CEL implementation types. Keep the path behind the internal option, set only by tests, and record 3.3 and its source map in the design when it ships.
Acceptance criteria
- Full records, selected scalars, nested maps/lists, wildcard and cross-namespace reads, and set reads return the same non-secret structure while tagged references participate in the Secret allowed set and refusals.
- Malformed tags, unsafe paths and tagged non-reference values fail with
secret_reference_invalidand no value in the error. Absent/null tagged fields and untagged reference-looking text retain their specified behavior. - Reading/adapting a record makes zero provider calls. Denied or revoked access fails before the evaluation result is consumed, including when another field was selected. A missing key remains a reference until delivery, where the load fails.
- Scalar selection and lazy getter tests prove the adapter does not read unrelated getters or mutate cached inputs.
.contentand.attributesretain their aliasing for JSON resources. - The container-rule tests distinguish a Secret actually read during CEL evaluation from one encountered only during result conversion. Existing absorption and refusal behavior remains intact.
- Data-origin results and compositions round-trip through registered sentinels with their origin preserved. Trusted delivery receives the exact canary value, including short, empty, Unicode and reference-shaped values.
- Extension
readResourceretains its trusted plaintext contract and existing legacy fallback. Raw non-JSON content remains unchanged. Option-off evaluator behavior is unchanged.
Use in-process unit tests beside the source, property tests for adaptation/boundary invariants, and integration tests wiring real records, access control, the registry and delivery. Reuse integration/canary_values.ts; compiled-binary journeys belong in swamp-uat.
Later slices
The shared adapter will also serve Phase 3.5 model.method results. Phase 4 owns the query environment and predicate/projection handling in Section 6.2, live/reconstructed workflow and child outputs, workflow passes and resume. Phase 3.4 owns destination policy and shell handling. This issue does not enable typed secrets in production or change remote transport.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.