fix: skip unresolvable cross-model resource expressions in globalArguments#644
fix: skip unresolvable cross-model resource expressions in globalArguments#644
Conversation
…ments (#641) When a model's globalArguments contain CEL expressions referencing another model's resource or file data (e.g., model.ssh-key.resource.state...), method execution fails with 'No such key: resource' if the referenced model has no data on disk. This extends the existing 'skip unresolved inputs' pattern in ExpressionEvaluationService.evaluateDefinition() to also skip expressions with model.*.resource or model.*.file references when the context proves that data doesn't exist. The raw expression is preserved in the definition so the Proxy on globalArgs throws a clear error if the method actually accesses the field, while methods that don't need it succeed. Also fixes a pre-existing bug in the globalArgs Proxy: previously, it only checked for unresolved inputs.* references, so a globalArg containing an unresolved model.*.resource expression would silently return the raw ${{ ... }} string to the method, risking silent data corruption. The Proxy now catches any remaining unresolved expression.
There was a problem hiding this comment.
Review Summary
This is a well-designed fix that follows established patterns in the codebase. No blocking issues found.
Code Quality ✅
- TypeScript strict mode compliance: No
anytypes used - Named exports: Used throughout
- Code follows existing patterns (extends the input-skip logic already present)
Domain-Driven Design ✅
- Changes are properly contained within domain services (
ExpressionEvaluationService,DefaultMethodExecutionService) - The Proxy pattern for lazy validation is a sound design choice - methods that don't need certain fields succeed, while accessing unresolved fields fails fast with clear errors
- The extension of skip logic for
model.*.resource/model.*.filemirrors the existinginputs.*pattern
Test Coverage ✅
- 3 new unit tests in
method_execution_service_test.ts:- Proxy throws for unresolved expressions
- Proxy allows access to resolved fields
executeWorkflowskips validation for unresolved resource expressions
- 1 new integration test verifying end-to-end behavior with unresolvable cross-model resource expressions
- Integration test properly uses dot notation (
model.source-shell.resource...) which matches theextractDependenciesregex
Security ✅
- Fixes a silent data corruption bug where unresolved non-input expressions could leak through the Proxy as raw strings
- No new security vulnerabilities introduced
Documentation ✅
- Troubleshooting documentation updated with new error message and resolution steps
Suggestions (non-blocking)
-
Consider a unit test for
evaluateDefinition(): While the integration test covers the code path, a direct unit test inexpression_evaluation_service_test.tsfor skipping unresolvable resource references would make the behavior more explicit and isolated. -
Bracket notation limitation: The
extractDependenciesregex only matches dot notation (model.name.resource) not bracket notation (model["name"].resource). This is a pre-existing limitation and not introduced by this PR - the fix is still an improvement over the previous state.
LGTM! The fix is well-reasoned, well-tested, and follows the established patterns in the codebase.
🤖 Generated with Claude Code
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
extractDependenciesdoesn't handle bracket notation -src/domain/expressions/dependency_extractor.ts:54-55The
MODEL_REF_PATTERNregex only matches dot notation:/model\.([a-zA-Z0-9_-]+)\.(input|resource|file|execution|definition)/gWhat breaks: Expressions written with bracket notation like
model["ssh-key"].resource.state.xwon't be detected by the new skip logic inevaluateDefinition. WhenextractDependencies(expr.celExpression)is called atexpression_evaluation_service.ts:236, it returns an empty array for bracket notation, so the skip logic doesn't trigger.Concrete example: If a definition contains:
globalArguments: vpc_id: "${{ model[\"my-vpc\"].resource.state.vpc.attributes.VpcId }}"
And
my-vpcwas never executed, the expression WON'T be skipped becauseextractDependenciesreturns[]. The CEL evaluator will then crash with "No such key: resource" - the exact error this PR aims to prevent.Evidence this is used: The codebase has existing tests using bracket notation:
integration/cel_data_access_test.ts:860:model['s3-infra'].resource...integration/cel_data_access_test.ts:986:model["s3-infra"].resource...
The CEL evaluator itself recognizes bracket notation (line 389):
/model(?:\.\w[\w-]*|\["[^"]+"\])\.(?:resource|file)\b/Mitigating factors:
- Most users write dot notation (
model.ssh-key.resource) which IS handled (regex allows hyphens) - The Proxy fallback at runtime will catch unresolved expressions if the method accesses them
- The failure is noisy (CEL crash), not silent corruption
Suggested fix: Update
MODEL_REF_PATTERNto also match bracket notation, similar to the pattern incel_evaluator.ts:389. -
Unit tests don't exercise the
evaluateDefinitionskip logic -src/domain/models/method_execution_service_test.ts:1536-1650All three new unit tests call
service.execute()orservice.executeWorkflow()directly with pre-built definitions. They test the Proxy behavior and validation skipping, but don't verify thatevaluateDefinitioncorrectly skips expressions with unresolvable resource dependencies.What this means: If someone breaks the skip logic in
expression_evaluation_service.ts, these tests won't catch it. The integration test does cover this path (using dot notation), but a unit test forevaluateDefinitionwould provide faster feedback.
Low
-
Potential false positive on evaluated strings containing
${{ ... }}-method_execution_service.ts:141-146The new Proxy throws for ANY string containing
${{ ... }}:if (typeof value === "string" && containsExpression(value)) { throw new Error(`Unresolved expression in globalArguments.${prop}: ${value}`); }
Edge case: If a CEL expression evaluates to a string that legitimately contains
${{ ... }}(e.g.,${{ 'Template: ${{ placeholder }}' }}), the Proxy would incorrectly throw.Why this is LOW: This is extremely unlikely in practice and arguably indicates a bug in the definition. The trade-off (preventing silent corruption) is worth it.
Verdict
PASS - The PR correctly extends the existing skip pattern for inputs.* expressions to also cover model.*.resource and model.*.file expressions. The Proxy fix is a genuine improvement that prevents silent data corruption. The bracket notation gap in extractDependencies is a pre-existing limitation that should be addressed in a follow-up, but doesn't block this fix since:
- Dot notation (the common case) works correctly
- The Proxy provides a runtime safety net
- The failure mode for bracket notation is visible, not silent
Summary
Fixes #641. When a model's
globalArgumentscontain CEL expressions referencing another model's resource or file data (e.g.,${{ model.ssh-key.resource.state.key.attributes.id }}), method execution crashes withNo such key: resourceif the referenced model has no data on disk. This prevents methods likeupdatefrom running even when they don't need the unresolvable field.What Changed
1. Expression evaluation skips unresolvable resource/file references
In
ExpressionEvaluationService.evaluateDefinition(), expressions referencingmodel.*.resourceormodel.*.fileare now skipped when the context proves the data doesn't exist (the referenced model was never executed or its data was cleaned up). This extends the existing pattern that already skips unresolvedinputs.*references.The raw
${{ ... }}expression is preserved in the definition — it is not silently discarded.2. Broadened unresolved-expression detection in globalArgs validation
In
DefaultMethodExecutionService.executeWorkflow(), the check for unresolved globalArgs fields now catches any remaining${{ ... }}expression, not justinputs.*ones. This is safe because vault/env expressions are always resolved beforeexecuteWorkflow()is called (both in the CLI path viamodel_method_run.ts:282and the workflow path viaexecution_service.ts:402).3. Fixed pre-existing silent data corruption bug in globalArgs Proxy
The Proxy on
context.globalArgspreviously only threw for unresolvedinputs.*expressions. If a globalArg contained an unresolvedmodel.*.resourceexpression, the Proxy would silently return the raw${{ ... }}string to the method. A model method could unknowingly send that literal string to an API. The Proxy now throws for any unresolved expression:Design Rationale
Follows an established pattern. The codebase already skips unresolved
inputs.*expressions inevaluateDefinition()(lines 214-229). Themodel.*.resourcecase is structurally identical — an expression references context data that doesn't exist yet. Extending the same skip logic is a natural, consistent extension.The safety net already exists. The Proxy on
context.globalArgscatches any attempt to use an unresolved expression at runtime. The flow is:${{ ... }}in the definitionbuildContext()already loads data from disk. WhendataRepois provided (which it is in both CLI and workflow paths),ModelResolver.buildContext()eagerly populatesmodel.*.resourcefrom on-disk data. If the data exists, the expression resolves normally. The skip only triggers when data genuinely doesn't exist.Alternative approaches are worse:
model.*.resourcepersistently available" — already happens; the problem is the referenced model has no data at alldata.latest()in definitions" — pushes complexity to usersUser Impact
globalArgumentscan run methods that don't need those fields (e.g.,updatedoesn't needssh_keys).Unresolved expression in globalArguments.ssh_keys: ${{ ... }}instead of the crypticNo such key: resource.Files Changed
src/domain/expressions/expression_evaluation_service.tsmodel.*.resource/model.*.filedepssrc/domain/models/method_execution_service.tssrc/domain/models/method_execution_service_test.tsintegration/keeb_shell_model_test.ts.claude/skills/swamp-model/references/troubleshooting.mdTesting
/tmp/swamp-641-test