Relationships
#3551 typed-secrets 1.2: pin the Secret Proxy, refusal set and container rule on cel-js 7.6.1
Opened by zack · 10/16/2026· Shipped 10/16/2026
Outcome
The Phase 1.2 tests in design/enablers/typed-secrets.md exist and pass against
@marcbachmann/cel-js 7.6.1. They show that the design's Secret value and its
refusals work on the pinned dependency. If one does not work, the test records
the fallback the doc names. The answer tells Phases 3 and 4 which shape to
build: the Proxy-based Secret (Section 3.1), or the plain class with private
fields, with the container rule extended to comprehension macros.
Why now
Phase 1.1 (swamp-club#3510) pinned what cel-js does today (Section 4.2). Phase
1.2 pins what the design needs cel-js to do with a Secret registered.
Phases 3 and 4 depend on its exit, and Phase 2 does not, so it can run beside
Phase 2.
This issue replaces swamp-club#3517. That issue described the old Phase 1.2: an async CelVaultNamespace receiver, loaded and unloaded secrets, and equality on loaded secrets. The cut plan (swamp-club#3545) dropped all of those.
Scope
Add pin tests beside the Phase 1.1 suite in
src/infrastructure/cel/cel_js_pins/. Build a test fixture Secret that has
the shape Section 3.1 specifies:
- a Proxy over a frozen, empty object, with its state in a module-private WeakMap;
ownKeysandgetOwnPropertyDescriptortraps that throw;- a
gettrap that answersconstructor, returns throwing functions fortoString,toJSON,valueOfandSymbol.toPrimitive, and returns undefined for every other key; - every throwing trap and function records a fault in an AsyncLocalStorage-scoped fault context before it throws.
This is a test fixture, not production code. Nothing outside the pin directory imports it.
Each test pins one claim from the Phase 1.2 list:
- cel-js recognizes the Proxy-based
Secretby its constructor. Building a macro range over it fails, and that failure is not absorbed. - A custom function sees, at its start, every fault recorded earlier in the same evaluation, across asynchronous evaluation. The effect gate in Section 4.5 depends on this.
{S: 1}andm[S]record a fault through a Proxy trap, so a map-key refusal is visible under||even though a map literal calls no overload.- An async custom function can return a
Secret, and awaiting it readsthenwithout a fault. - The refusing overload set covers every built-in that accepts a string or a
dyn value:
- It includes
==between two secrets. There is one refusingSecret == Toverload for each type cel-js accepts in a signature, plus aSecret == nulloverload that answers false in both operand orders. - Adding
Secret == SecretbesideSecret == dyndoes not remove the overlap. Record that the enumerated set is the only shape that works.
- It includes
- A recorded fault survives absorption by
||,&&,existsandall, across asynchronous evaluation. - Synchronous entry points still return synchronous results with the
Secretregistrations in place. All refusing handlers are synchronous, and functions use{ async: false }. vault.get, with the static return typeSecret, gives check-time errors in the validation environment for everything outside the allowed set in Section 4.1. The allowed set is passing along, concatenation, comparison with null, andhas()or.?.- The container rule in Section 4.3, item 5, refuses every case the fact-2
pins answer by identity (cel_js_equality_fallback_pin_test.ts). This needs
two things:
- cel-js reads a record field, a list element and a map value through ordinary property access, so a getter records the secret on selection and on container traversal.
- Map key membership, and selecting a different field, do not record it.
Also add the refusal-coverage fitness test that Section 4.3 describes. It
lists the internal environment's built-ins, plus swamp's own registrations in
the internal environment and the query view (the data, file and model
namespaces). It fails when a string, dyn or list<dyn> parameter has neither
a Secret refusal nor a named delivery (model.method inputs). Decide, and
write into the test, whether the built-in list comes from cel-js or from a
maintained list pinned against 7.6.1. The == type list is maintained either
way, because cel-js does not export its type table.
Follow the Phase 1.1 conventions:
- Name each test with
pin(...)from cel_js_pin_test_helpers.ts, and quote the doc claim it pins in the file header. - Run in-process: no subprocesses, sockets or env mutation.
- Add license headers with
deno run license-headers.
Exit
Every claim above is observed by a passing test. If a claim fails:
- Record that in a pin.
- Switch the doc to the Section 3.1 fallback (a plain class with private fields), with the container rule covering comprehension macros.
- Update the Phase 3 and Phase 4 scope in the doc to match.
Out of scope
- Production code. No
Secrettype, adapter or registration enters src/infrastructure/cel/cel_evaluator.ts or any production path. Phase 3 builds those. - Phase 2 (sentinels hold references), which does not depend on this.
- Phases 3 to 5, which get their own issues after this exit.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.