Skip to main content
← Back to list
01Issue
FeatureShippedSwamp CLIPublic
Assigneeszack

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 Secret value (Section 3.1). A production module in src/infrastructure/cel/ with one Secret class, registered with registerType("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 a Secret by 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 AsyncLocalStorage fault context for each evaluation, as RunVaultAccess does. A recorded fault fails the evaluation after any absorption by ||, &&, exists or all, 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, null included, synchronous handlers and { async: false } functions. Equality is one refusing Secret == T overload per nameable type, plus Secret == null answering false. Swamp's own registrations in the internal environment (the data, file, model and workers namespaces) get their refusals too, and the vary list of data.latest, data.version and data.listVersions gets the argument walk that refuses with secret_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 or null operand. During evaluation, the fault context records each Secret that 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 a Secret fails with secret_operation_unsupported, after any absorption. The boundary conversion of the result reads getters without recording.
  • The boundary conversion (Section 3.2). A Secret that leaves CEL becomes its sentinel, issued into the run's SecretBagRegistry. 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 a Secret. Text with any other prefix stays text. The conversion runs before coerceBigInts and unwrapOptional, converts a Secret inside 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 a Secret.
  • Error codes. secret_operation_unsupported and secret_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 Secret registrations, 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 ||, &&, exists and all, and with two secrets as operands.
  • The allowed set works: passing along, + in both operand orders and composed, == null and != null in 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 Secret result round-trips through the boundary conversion: out as a sentinel the registry resolves to its reference, and back in as a Secret. 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.get as a CEL function returning Secret, the bare-token rewrite, the AST extractor that replaces findVaultGetCalls on this path, and the up-front existence check (Sections 3.4 and 4.4).
  • 3.3: the accessor adapter for data.* and model.*.resource, and secret_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 evaluate writing its cache through the conversion, the validate diagnostics with the allowed set's overloads, context.runModel, and the model.method rules (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).

02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 5 MOREREVIEW+ 12 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

10/19/2026, 7:03:24 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
zack assigned zack10/19/2026, 5:22:07 PM

Sign in to post a ripple.