Relationships
#3625 typed-secrets 3.2: vault.get as a CEL function, bare-token rewriting, AST extraction and up-front existence checks
Opened by zack · 10/19/2026· Shipped 10/19/2026
Outcome
With the internal typedSecrets option enabled, vault.get(string, string) is a real CEL function that returns a production Secret reference. It constructs no plaintext value. The existing boundary conversion carries the result out of CEL as a sentinel, and the Phase 2 delivery path remains responsible for loading it.
Before an expression runs, every direct vault.get call has its arguments evaluated and its reference checked, including calls in unselected branches. Missing or unreadable references keep failing at evaluation rather than disappearing behind branch selection or a null comparison.
This is Phase 3.2 of design/enablers/typed-secrets.md, specifically Sections 3.4, 4.4 and 11. It follows swamp-club#3619, the Phase 3.1 production Secret and CEL boundary core, and reuses the Phase 2 existence and loading contracts from #3557, #3558 and #3562.
Scope
- The CEL receiver. Register
vaultwithget(string, string): Secretin the internal typed environment. A call creates a reference through the production Secret factories and participates in the existing fault context, container rule, refusals and boundary conversion. Refuse aSecretin either argument. The function itself reads no secret value. - Legacy argument compatibility. Before parsing, use the template scanner to quote only bare tokens that today's
isDynamicVaultArgumenttreats as literal names. Preservevault.get(my-vault, KEY)without interpreting the hyphen as subtraction. A bare token containing member access remains CEL. Preserve quoted strings and today's accepted argument behavior; do not rewrite text inside string literals. - One AST extractor. After the bare-token rewrite, extract direct
vault.getcalls from the parsed expression. ReplacefindVaultGetCallson the typed path and migrate its consumers to the shared extraction semantics: the existence check,vaults:list validation, foreign-root attribution, the dispatch allowlist, andfreeRootsin the parse-only grammars. Preserve authored locations for diagnostics. Keep option-off behavior compatible while the legacy execution path remains in use. - Namespace restrictions. Preserve
assertNoUnresolvedVault's rule thatvaultis usable only through a directvault.getcall. Refuse aliases such ascel.bind(v, vault, v.get(...)),dyn(vault).get(...)and[vault].map(m, m.get(...)), so no reference can bypass up-front extraction and checking. - Up-front checks. Evaluate the arguments of every extracted call before executing the expression, whichever branch would run, with today's accepted-argument rules. Argument failures fail the expression. Reuse
checkSecretReference,RunVaultAccessand the run'sSecretBagRegistry: authorize each check, use the provider'shascontract where available, otherwise make at most one probe per reference per run. Register a probe's value with the redactor and immediately discard it. Cache only positive existence results, coalesce in-flight checks, and never cache authorization. - Errors and boundaries. Missing references report
secret_reference_unavailablewith the vault, key and authored location. Access denials and provider failures retain the existing contract. No plaintext enters CEL or a bag during checking. A refused expression makes zero delivery loads; a permitted existence probe is counted separately. Keep Secret-specific types inside the CEL infrastructure adapter and use the existing domain contracts across the boundary. - Internal rollout and documentation. Keep the path behind the existing internal option, set only by tests. Update the architecture fitness and refusal-coverage checks for the added namespace without enabling a production entry point. Record Phase 3.2 and its source map in the design when it ships.
Acceptance criteria
- Through
CelEvaluatorwith the option on, directvault.getcalls produce production Secrets and leave CEL as registered reference sentinels. Passing, concatenation and null comparisons follow the allowed set; unsupported operations retain the existing value-free refusal behavior. - Quoted arguments, legacy bare vault/key names, hyphenated names and accepted dynamic member expressions retain their intended meaning. Literal text containing
vault.get(...)is not treated as a call. Parsing tests cover nested syntax, escapes and authored locations. - The shared extractor gives the existence check, vault-list validation, foreign-root analysis, dispatch allowlist and parse-only root analysis consistent results. Vault aliases cannot bypass it.
- A missing or unauthorized reference in an unselected branch fails before expression execution. An argument evaluation failure does the same. A revoked grant fails the next check even after a cached positive existence result.
- Counting providers prove zero value reads for checks backed by
has, at most one fallback probe per reference per run including concurrent checks, and zero delivery loads for refused expressions. Probed plaintext never enters a bag or CEL and is absent from errors and diagnostics. - A permitted result reaches the existing load-at-delivery path as a reference and delivers the exact canary value. Repeated checks do not turn probes into delivery values.
- The refusal inventory and production coverage test include the vault namespace and both secret-valued argument positions. The Phase 3.1 boundary, absorption and container-rule suites continue to pass.
- With the option off, existing evaluator behavior and registrations remain unchanged, and no production entry point enables typed secrets.
Use in-process unit tests beside the source, property tests for rewrite/extraction invariants where useful, and integration tests for the evaluator, reference checks, registry and delivery contracts. Reuse integration/canary_values.ts and the existing provider conformance contracts. Compiled CLI acceptance journeys belong in swamp-uat.
Later slices
Phase 3.3 owns the accessor adapter for data.* and model.*.resource, sensitive-field authorization and secret_reference_invalid. Phase 3.4 owns destination refusals and masking. Phase 3.5 owns behavior evaluate/validate entry points, context.runModel and the model.method delivery rules. Workflows, queries, resume and enabling the path globally remain Phase 4; remote reference transport remains Phase 5. Migrating the dispatch allowlist's extractor here does not change that remote protocol scope.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.