fix: preserve vault CEL expressions during model type upgrades#1168
fix: preserve vault CEL expressions during model type upgrades#1168
Conversation
The upgrade persistence path in executeWorkflow persisted the in-memory definition after vault expressions had been resolved to per-process sentinel tokens. Subsequent runs read these literal sentinels from YAML, which could never be resolved — causing auth failures and data corruption. Re-read the original definition from the repository (which has vault CEL expressions intact) before applying the upgrade and persisting. The sentinel-containing in-memory definition continues to be used for execution unchanged. Closes swamp-club#91 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
Clean, well-motivated bug fix. The problem is clear: when a model type version bump triggers a definition upgrade, the in-memory definition already has vault CEL expressions replaced with per-process sentinel tokens. Persisting those sentinels corrupts the YAML and breaks subsequent runs.
The fix correctly re-reads the on-disk definition (via findById), applies the upgrade to that pristine copy, and persists it — keeping vault expressions intact.
Blocking Issues
None.
Suggestions
- Test narrowing cast (
method_execution_service_test.ts:2616): Theas unknown as Definitiondouble-cast could be replaced withassertExists(savedDefinition)from@std/assert, which narrows the type and makes the assertion more idiomatic. Not blocking since the current approach works correctly in test code.
DDD Assessment
- Domain Service placement: The fix lives in
DefaultMethodExecutionService, which is the correct domain service for orchestrating definition upgrades and persistence — no misplaced responsibilities. - Repository pattern: Re-reading the pristine definition via
DefinitionRepository.findByIdis exactly the right approach — the repository is the authority on persisted state, and the domain service correctly delegates to it rather than trying to track "original vs. resolved" state internally. - Separation of concerns:
DefinitionUpgradeServiceremains a pure, stateless domain service that takes a Definition and returns an upgraded copy. The persistence concern stays in the orchestrating service. Good separation.
Other Notes
- The
if (originalDefinition)null guard at line 350 correctly handles the edge case where the definition was deleted between workflow start and upgrade persistence. - No libswamp import boundary violations.
- License headers present on all modified files.
- Both unit tests are well-structured and follow the project's naming conventions.
- This is also a security improvement — it prevents vault sentinel tokens (representing secrets) from being written to YAML files on disk.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None.
Medium
None.
Low
-
Silent no-op when
findByIdreturns null (src/domain/models/method_execution_service.ts:350): If the on-disk definition has been deleted between the in-memory load and the re-read (findByIdreturnsnull), the upgrade is silently not persisted. The next run will re-trigger the upgrade and succeed (self-healing), so this is not a correctness problem. Alogger.warn(...)here would aid debugging of unusual repository states, but it's not required. -
Redundant save if concurrent process already upgraded (
src/domain/models/method_execution_service.ts:351-358): The result ofdiskUpgrade.upgradedis not checked beforesave(). If another process already upgraded the on-disk definition to the target version,diskUpgrade.upgradedwould befalseand the save is a no-op write of the same data. Harmless — the upgrade is idempotent — but aif (diskUpgrade.upgraded)guard would save one unnecessary I/O.
Verdict
PASS — The fix is correct and well-targeted. The core logic is sound: re-reading from disk avoids persisting per-process vault sentinel tokens, the DefinitionUpgradeService.upgrade() is pure (returns new Definition via withUpgradedGlobalArguments, doesn't mutate), globalArguments returns structuredClone so upgrades on the disk copy can't corrupt the in-memory execution copy, and the null path from findById is handled. Both new tests cover the right things: the unit test verifies vault expressions survive the upgrade service, and the integration-style test verifies the full executeWorkflow path persists vault expressions instead of sentinels.
Summary
executeWorkflowto re-read the original definition from the repository before persisting, preventing vault sentinel tokens from leaking into model YAMLCloses swamp-club#91
Test Plan
/tmp/swamp-repro-issue-91— confirmed sentinel__SWAMP_VSEC_*was written to YAML after version bumpexecuteWorkflow: upgrade persists original vault expressions, not sentinel tokensDefinitionUpgradeService: vault expression strings pass through unchangedwhoami_test.ts)🤖 Generated with Claude Code