Relationships
#3619 typed-secrets 3.1: the production Secret, its refusals, the fault context, the container rule and the boundary conversion, behind the internal option
Opened by zack · 10/19/2026· Shipped 10/19/2026
Outcome
The internal CEL evaluator (CelEvaluator, src/infrastructure/cel/cel_evaluator.ts)
can hold a production Secret value and refuse every operation outside the
allowed set in design/enablers/typed-secrets.md Section 4.1, with a stable,
value-free error, even under ||, &&, exists and all. A Secret leaves
CEL as a sentinel and a bound sentinel enters CEL as a Secret. All of this sits
behind an internal option that only tests set; with the option off, the
evaluator is exactly what it is today. This is the foundation the rest of
Phase 3 builds on.
Why now
Phases 0, 1 and 2 have shipped (swamp-club#3510, #3511, #3512, #3551, #3557,
#3558, #3562). Phase 1.2 proved the Proxy-based Secret, the refusal set and
the container rule on cel-js 7.6.1 with a test fixture. Every other Phase 3 item
(Section 11) needs a production Secret to exist first: vault.get returns
one, the accessor adapter produces them, the destination refusals and masks
consume their sentinels, and the model.method effect gate reads the fault
context. This issue builds that core and nothing that produces a Secret from a
real source.
Scope
- The
Secretvalue (Section 3.1). A production module in src/infrastructure/cel/ with oneSecretclass, registered withregisterType("Secret", { ctor, fields: {} }). Each instance is a Proxy over a frozen, empty object, with its state in a module-private WeakMap: a vault reference, a sensitive-field reference marked data origin, or a composition (an ordered list of text and references). The traps and throwing functions are the ones Section 3.1 specifies, and each records a fault before it throws. Only module-owned factories create instances. Nothing recognizes aSecretby shape. The Phase 1.2 pins switch from the fixture in cel_js_secret_test_helpers.ts to the production type, so a dependency upgrade is checked against what ships, and the fixture goes. - The fault context (Section 4.3 item 2). The evaluator enters an
AsyncLocalStoragefault context for each evaluation, asRunVaultAccessdoes. A recorded fault fails the evaluation after any absorption by||,&&,existsorall, and across asynchronous evaluation. - The refusals (Section 4.3 item 1). The refusal inventory
(cel_js_secret_refusal_inventory.ts) moves from cel_js_pins/ to
src/infrastructure/cel/. The evaluator registers the refusing overloads it
yields: the full cross product over each signature's refusal positions, every
operator against every type cel-js or swamp can name,
nullincluded, synchronous handlers and{ async: false }functions. Equality is one refusingSecret == Toverload per nameable type, plusSecret == nullansweringfalse. Swamp's own registrations in the internal environment (thedata,file,modelandworkersnamespaces) get their refusals too, and the vary list ofdata.latest,data.versionanddata.listVersionsgets the argument walk that refuses withsecret_destination_refused. - The container rule (Section 4.3 item 5). Before evaluation, scan the
parsed expression for a container comparison: an
in, or an==or!=with no scalar-literal ornulloperand. During evaluation, the fault context records eachSecretthat enters it from a binding or is read out of a record, list or map, through getters that the boundary conversion puts on each secret-valued property and element. An evaluation that has a container comparison and records aSecretfails withsecret_operation_unsupported, after any absorption. The boundary conversion of the result reads getters without recording. - The boundary conversion (Section 3.2). A
Secretthat leaves CEL becomes its sentinel, issued into the run'sSecretBagRegistry. A composition becomes its text with a sentinel for each reference. A string bound into CEL that holds sentinels the run's registry issued becomes aSecret. Text with any other prefix stays text. The conversion runs beforecoerceBigIntsandunwrapOptional, converts aSecretinside an Optional, and builds new objects; it never wraps or mutates a context, a cached record or a live output in place. No code outside the CEL adapter sees aSecret. - Error codes.
secret_operation_unsupportedandsecret_destination_refused, as stable codes. No error message, cause or fault carries a value. - The internal option (Section 10.3). One evaluator option, set only by
tests, turns on the
Secretregistrations, the fault context, the container rule and the conversion. With it off, the internal environment registers nothing new: a custom binary overload marks every use of its operator maybe-async (fact 4), so the registrations must not be there when the option is off. - The production half of the coverage test (Section 4.3).
integration/cel_secret_refusal_coverage_rules_test.ts also asserts that
getDefinitions()on the internal environment, with the option on, contains every refusal signature the inventory yields. - Doc. Section 4.3 says the production half also covers the query view, but the query view (Section 6.2) is Phase 4. Change that sentence so the query-view half moves with Section 6.2 into Phase 4. Record 3.1 in Section 11 and the source map when it ships.
Follow the repository conventions: in-process unit tests beside the source,
property tests (*_property_test.ts) for the conversion round trip and
composition, the canary classes in integration/canary_values.ts for value-free
errors, and license headers with deno run license-headers.
Exit
With the option on:
- Every refusal the inventory yields is observed through
CelEvaluator, with its stable code and no value in the error, including under||,&&,existsandall, and with two secrets as operands. - The allowed set works: passing along,
+in both operand orders and composed,== nulland!= nullin both orders,has()and.?. - Each container comparison case the fact 2 pins list is refused, and the over-refusal cases in Section 4.3 fail loudly and name the expression.
- A
Secretresult round-trips through the boundary conversion: out as a sentinel the registry resolves to its reference, and back in as aSecret. Sentinel-shaped text with another prefix stays text. - Synchronous entry points still return synchronous results.
- The production half of the coverage test passes.
With the option off, existing suites pass unchanged and the internal
environment's getDefinitions() equals today's.
Out of scope
Each is a later Phase 3 issue:
- 3.2:
vault.getas a CEL function returningSecret, the bare-token rewrite, the AST extractor that replacesfindVaultGetCallson this path, and the up-front existence check (Sections 3.4 and 4.4). - 3.3: the accessor adapter for
data.*andmodel.*.resource, andsecret_reference_invalid(Section 6.1). - 3.4: destinations.
.meta({ secrets: "refuse" }), the identifier stand-ins taken from sentinels, the shell refusals, and masking in reports, events, logs and errors (Sections 6.3 and 7). - 3.5: entry points.
swamp behavior evaluatewriting its cache through the conversion, thevalidatediagnostics with the allowed set's overloads,context.runModel, and themodel.methodrules (Sections 4.3 item 4, 4.5 and 5).
Also out of scope: the query view, workflows and resume (Phase 4), and remote dispatch (Phase 5).
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.