Skip to main content
← Back to list
01Issue
BugShippedSwamp CLI
AssigneesNone

#291 Workflow-level runtime expressions (env.*, vault.*) not resolved in driverConfig — docker driver receives literal ${{ ... }} strings

Opened by john · 5/7/2026· Shipped 5/12/2026

Summary

${{ env.* }} and ${{ vault.* }} runtime expressions inside a workflow's driverConfig.volumes are never resolved before the docker driver builds its argv. Docker receives the literal string ${{ env.HOME }} (etc.) and rejects it.

This forces every workflow that needs to mount a per-user host path (e.g. credentials at ${HOME}/.foo, the current repo at ${PWD}) to hardcode an absolute path, which makes the workflow non-portable across machines and users.

Relation to #263

This is one layer earlier than #263. In #263, vault expressions in step inputs were being resolved into __SWAMP_VSEC__ sentinels but the sentinels weren't unwrapped before the docker invocation — the model received the sentinel string. Here, the runtime expression is in workflow driverConfig (not step inputs), so it never enters the runtime resolver at all: it survives WorkflowExpressionEvaluator (which deliberately skips runtime expressions, see below), there is no subsequent runtime-resolution pass over workflow data, and the literal ${{ env.HOME }} reaches docker -v untouched. The #263 fix operates downstream of the resolver, but the resolver was never run for these fields.

Steps to reproduce

  1. Create a command/shell model instance:
    swamp model create command/shell test-shell
  2. Drop a workflow file into workflows/workflow-<uuid>.yaml with a runtime expression in driverConfig.volumes:
    id: <a-valid-uuidv4>
    name: test-env-vol
    description: Verify env-substitution in driverConfig.volumes.
    version: 1
    tags: {}
    inputs: {}
    jobs:
      - name: print
        dependsOn: []
        weight: 0
        steps:
          - name: ls
            driver: docker
            driverConfig:
              image: alpine:3
              volumes:
                - ${{ env.HOME }}:/host-home:ro
            task:
              type: model_method
              modelIdOrName: test-shell
              methodName: execute
              inputs:
                run: "ls /host-home | head -1"
            dependsOn: []
            weight: 0
            allowFailure: false
  3. Run it:
    swamp workflow run test-env-vol

Expected

The docker driver receives the resolved volume string, e.g. /Users/me:/host-home:ro, and the container starts.

Actual

The docker driver receives the literal ${{ env.HOME }} and docker rejects it:

docker: Error response from daemon: create ${{ env.HOME }}: "${{ env.HOME }}"
includes invalid characters for a local volume name, only
"[a-zA-Z0-9][a-zA-Z0-9_.-]" are allowed. If you intended to pass a host
directory, use absolute path

The same expression in workflow inputs flowing into a model method does get resolved correctly — only driverConfig (and other non-input workflow-level fields) is affected.

Source diagnosis

Looking at the source on 20260505.231643.0-sha.5a337b81:

  • Model Definition data goes through two evaluation passes:

    1. DefinitionExpressionEvaluator.evaluate() (CEL-only) at src/domain/workflows/expression_evaluators.ts.
    2. ExpressionEvaluationService.resolveRuntimeExpressionsInDefinition() called from src/domain/workflows/execution_service.ts (around the model-method execution path), which resolves env.* and vault.*.
  • Workflow data goes through only the first pass:

    • WorkflowExpressionEvaluator.evaluate() at src/domain/workflows/expression_evaluators.ts:58–122 extracts all expressions, but at line ~89 explicitly skips runtime expressions:
      if (containsRuntimeExpression(expr.celExpression)) {
        continue;
      }
    • There is no equivalent second pass that calls resolveRuntimeExpressionsInData over the workflow data before per-step driverConfig is forwarded to the driver (see execution_service.ts where step.driverConfig / job.driverConfig are pulled into the DriverPlan unchanged).

So workflow-level ${{ env.* }} and ${{ vault.* }} are only ever evaluated for fields that subsequently flow into a model Definition (via task.inputs). For workflow fields that go straight to the driver — driverConfig.image, driverConfig.volumes, driverConfig.env, driverConfig.extraArgs, etc. — the expressions are never substituted and the driver gets the raw ${{ ... }} string.

Suggested fix

Add a runtime-expression resolution pass over the workflow data, mirroring the model-definition path:

  • Either inside WorkflowExpressionEvaluator.evaluate() after the CEL-only pass, or as a separate resolveRuntimeExpressionsInWorkflow() call invoked at workflow execution start (analogous to resolveRuntimeExpressionsInDefinition).
  • The walker (extractExpressions / replaceExpressions) and the runtime evaluator (ExpressionEvaluationService.resolveRuntimeExpressionsInData) already handle arbitrary nested data, so the missing piece is the second-pass invocation, not new walking logic.

Confirmed by inspecting the persisted .swamp/workflows-evaluated/workflow-*.yaml after a run — the file retains the literal ${{ env.HOME }} because it is saved post-CEL-pass, pre-runtime-pass, and there is no third file capturing post-runtime-pass workflow state (because that pass doesn't exist).

Environment

  • swamp 20260505.231643.0-sha.5a337b81
  • macOS (Darwin 22.6.0)
  • docker engine: standard Docker Desktop
  • Source inspected at the same version via swamp source fetch.
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPEDTRIAGE+ 5 MOREREVIEW+ 2 MOREPR_LINKEDCOMPLETE

Shipped

5/12/2026, 8:01:17 AM

Click a lifecycle step above to view its details.

03Sludge Pulse

Sign in to post a ripple.