Skip to main content
← Back to list
01Issue
FeatureClosedSwamp CLIPublic
AssigneesNone

Relationships

#3517 typed-secrets 1.2: minimal opaque Secret, CelVaultNamespace receiver, policy inventory and evaluation frame

Opened by zack · 10/15/2026

Outcome

The first engine-2 code: a minimal opaque Secret value object, a per-evaluation CelVaultNamespace receiver bound to a resolver port, a checked-in inventory of every exposed CEL operation with its secret policy, and an evaluation frame that records value-free faults outside CEL's error flow. All of it is dormant: nothing in a production code path imports it. It is the foundation the rest of Phase 1 (lowering in 1.3, tracked async work in 1.4) builds on and the Phase 1 gate (1.5) proves.

Why now

Design doc Phase 1 lists the opaque Secret, the CelVaultNamespace receiver, the overload and policy inventory and the evaluation frame as the work that follows committing the probe harness. The harness landed with swamp-club#3510, so the facts this issue relies on (constructor-identity typing, derived inequality, synchronous equality, fields: {} semantics, Object.keys comprehension) are now pinned tests, not claims. Lowering (1.3) needs the inventory to know what to lower and the frame to record into; async tracking (1.4) needs the receiver and the frame's check points. Neither can start without this.

Scope

Secret and SecretSource value objects (Section 3.2), under src/domain/secrets/ as Section 3.1 locates them; they do not depend on cel-js:

  • One concrete class. All state in private fields or a module-private WeakMap, so the object has no own enumerable properties. Module-owned factories and identity checks; no recognition of a user-supplied object shape.
  • Single-source or composed; loaded or unloaded. A composed secret is loaded only if every component is loaded in the current attempt. Values are immutable; loading produces a new scoped value. Reference identity never substitutes for comparison of contents.
  • toString, toJSON, valueOf and Symbol.toPrimitive throw a value-free UnrenderedSecretError. Custom inspection returns the literal text less-than secret greater-than without loading.
  • The only source in this slice is a validated vault reference (vault, key) with origin metadata. Inline and sensitive-field sources come later.

CEL adapter pieces under src/infrastructure/cel/ (Sections 3.3, 4.1, 4.3):

  • Secret registered once with registerType, name at least two characters, fields: {}.
  • A CelVaultNamespace receiver type with an async get(string, string) returning Secret. Each evaluation gets its own instance bound to that run's resolver port (authorize and load operations); never an environment-global handler. Secret arguments are refused by the typed signature in this slice; 1.3 makes that refusal a recorded frame fault before native overload resolution.
  • Overloads: register equality only, for Secret with string in both orders and Secret with Secret, so cel-js derives inequality and the mirror. Equality is a synchronous string comparison on the private loaded storage; an unloaded operand fails with secret_not_loaded. Addition in both operand orders and Secret plus Secret produce a composed Secret with no additional loading. size(S) and S.size() return the code-point count as a CEL int. type(S) is the type name; has(S.x) is false. Direct equality between a Secret and a non-string non-secret value is a value-free type refusal in both orders, including maps and null.
  • A resolver test double: in-memory, immediately resolved promises, recording authorize and load calls per evaluation so tests assert exact counts.

Policy inventory (Section 4.3): a checked-in table, as a TypeScript constant with a pinned-set test in the arch_fitness_helpers style, listing every exposed operator, function, macro receiver, index and key form, and dynamic map key, each classified as native delegate, allowed composition, scalar declassifier, or refuse. Rows whose refusal needs lowering (membership, container structural equality, comprehension range, map key, conversion, transform, matches) are marked for 1.3 and are not implemented here. Adding an exposed operation without a row fails the test.

Evaluation frame (Sections 3.1 and 4.3):

  • A per-evaluation record of policy faults, loads and invocations, held outside CEL's error flow.
  • Every policy check records a sticky value-free fault in the frame before it throws. The evaluation result is discarded on any recorded fault, so a policy error absorbed by an or-expression, all or exists still fails the evaluation.
  • Attempt identity and a cancellation slot. The namespace receiver checks the frame and cancellation before starting work and after awaited work, so a recorded fault stops a later namespace call from launching. With the immediate resolver double, the after-await check is exercised but the async-ordering cases belong to 1.4.
  • Loaded values are bound to their frame and attempt. The synchronous declassifiers (equality, size) verify ownership and liveness against the current frame; a value from another frame or an ended attempt is treated as unloaded.

Tests: unit tests beside each file, in-process, no sockets, subprocesses or env mutation; a property test (fast-check) for opacity and composition, in the style of cel_evaluator_property_test.ts; license headers via deno run license-headers.

Out of scope

  • AST lowering, the reserved helper namespace, rejection of aliasing or shadowing of vault, data and model roots, the comprehension-range type check, dynamic key refusal, and pinning secret matches as unsupported. All 1.3.
  • Tracked async work: rejection observers, controlled-promise start-order tests, async-start with synchronous-failure siblings, cancellation propagation, fake async loading with gates, concurrent and nested reuse, program isolation or a bounded program pool. All 1.4.
  • The Section 4.4 conformance matrix and the gate record (1.5).
  • The brand-aware walk of the Section 3.2 utility inventory, containsSecret over Optional, the value codec, and the secrets refuse schema metadata. All Phase 2.
  • Any change to cel_evaluator.ts, the grant-condition environment, the workflow-run filter, extension or raw-query environments, VaultService, the redactor or audit. No production path imports the new modules.
  • Inline and sensitive-field sources; a real VaultService-backed resolver.

Acceptance

  • Every row of the Section 4.1 table that does not require lowering has a named passing test on loaded, unloaded and composed secrets, in both operand orders where the row has two operands.
  • Opacity: one test per hook (toString, toJSON, valueOf, Symbol.toPrimitive, inspection, JSON.stringify, template interpolation); no own enumerable properties; comprehension over a Secret yields an empty range today, pinned as the behavior 1.3 must turn into a refusal.
  • Frame: an absorbed policy error fails the evaluation; a recorded fault stops a later namespace call; a loaded value from another frame or attempt is refused.
  • Two evaluations of one parsed program with different resolver instances see only their own values, and the recorded authorize and load counts match the selected branches exactly.
  • The policy inventory covers every exposed operation and its pinned-set test fails when one is added without a row.
  • deno info shows no import of the new modules from outside their own tests.
  • verify-build, verify-reviews and verify-skills pass at the reviewed commit.

References

  • Design doc Sections 2, 3.1 to 3.3, 4.1, 4.3, 4.4 and Phase 1, landed by swamp-club#3509.
  • Pinned cel-js behavior under src/infrastructure/cel/cel_js_pins/, landed by swamp-club#3510; the helper there shows the version-prefixed naming.
  • Current engine-1 environment setup in src/infrastructure/cel/cel_evaluator.ts (read for context; do not modify).
02Bog Flow
✓OPEN○TRIAGED○IN PROGRESS◉CLOSED

Closed

10/16/2026, 3:06:17 PM

No activity in this phase yet.

03Sludge Pulse
Editable. Press Enter to edit.

zack commented 10/16/2026, 3:06:17 PM

Closing as superseded. The typed-secrets plan is being cut in swamp-club#3545: secrets stay references in the current expression engine, and there is no engine 2.

This issue describes the old Phase 1.2: an async CelVaultNamespace receiver bound to a resolver, loaded and unloaded secrets, equality and size on loaded secrets, refusal of null comparisons, and the evaluation frame that lowering (1.3) and async tracking (1.4) build on. None of that is in the revised plan.

The revised Phase 1.2 pins different behavior on cel-js 7.6.1:

  • A Proxy-based Secret that holds only a reference, recognized by its constructor, whose macro range fails without being absorbed.
  • A refusing overload for every built-in that accepts a string or a dyn value. Equality is one overload per type, plus Secret == null answering false.
  • An ambient fault context that survives absorption by ||, &&, exists and all.
  • Synchronous entry points staying synchronous with the registrations in place.
  • Check-time errors for vault.get misuse in the validation environment, except comparison with null.

It will be filed as its own issue once swamp-club#3545 lands.

Sign in to post a ripple.