Skip to main content
← Back to list
01Issue
BugShippedSwamp CLIPublic
Assigneesstack72

Relationships

#3227 context.runModel does not resolve the target definition's ${{ }} expressions (vault/CEL), contrary to the design doc

Opened by dmc · 10/10/2026· Shipped 10/11/2026

Description

context.runModel({ definition: "<name>", method }) runs the target with its stored globalArguments exactly as written. ${{ … }} expressions in the target definition (vault references, CEL) are not resolved. The target method receives the literal expression text.

The same definition works through swamp model method run <name> <method> and through a workflow model_method step, because those paths resolve the definition's expressions before execution.

Steps to reproduce

  1. A model definition node-a (here @keeb/proxmox/node) with vault-backed global arguments:
    globalArguments:
      apiUrl: ${{ vault.get(<vault>, <URL_KEY>) }}
      username: ${{ vault.get(<vault>, <USER_KEY>) }}
      password: ${{ vault.get(<vault>, <PASSWORD_KEY>) }}
  2. swamp model method run node-a listVmSnapshots --input vmid:json=200 → works.
  3. From another extension's method (with the target extension in manifest dependencies):
    const res = await context.runModel({
      definition: "node-a",
      method: "deleteVmSnapshot",
      arguments: { vmid: 200, name: "snap-1" },
    });

Expected: the nested run resolves node-a's vault expressions, as the standalone run does.

Actual: res = { ok: false } with Invalid URL: '${{ vault.get(<vault>, <URL_KEY>) }}/api2/json/access/ticket'. The target received the raw expression string as apiUrl.

Documentation says this is not intended

  • design/primitives/models.md, "Cross-Model Invocation (context.runModel)": "Vault isolation: The caller's VaultSecretBag is not passed to the nested model. Each nested execution resolves its own vault expressions."
  • Same section, "Data ownership": behaviour "holds whether Y runs standalone, from a workflow or inside X".
  • The vault skill's trust-boundary table lists "Model definitions, workflow YAML → evaluated", with no exception for runModel.

Where in the code

  • src/domain/models/model_invocation_service.ts (invoke, ~line 245): let mergedGlobalArgs = definition.globalArguments ?? {}; is passed straight to buildMethodContext(… globalArgs: mergedGlobalArgs …) and executionService.executeWorkflow(childDefinition, …). No expression evaluation or vault resolution happens on this path.
  • resolveRuntimeExpressionsInDefinition is called only from src/libswamp/models/run.ts (CLI model method run) and src/domain/workflows/execution_service.ts (workflow steps).

Impact

Any extension that calls a vault-configured model through runModel breaks, silently for the author until runtime: e.g. snapshot / rollback / prune of Proxmox VMs through @keeb/proxmox/node. The swamp docs tell extension authors to use runModel instead of shelling out to the CLI (and the deno-command publish rule enforces it), so this pushes authors onto a path that cannot reach vault-configured models.

Workaround: for targets whose definition contains ${{, run swamp model method run as a subprocess instead of runModel.

Suggested fix

In ModelInvocationService.invoke, resolve the target definition's runtime expressions (vault + CEL) the same way the CLI / workflow paths do before building the child context, with the nested execution's own vault service (per the "Vault isolation" design note), and redact resolved secrets in its output as usual.

Environment

  • swamp 20261009.175905.0-sha.67799a63; the code is unchanged in the latest 20261009.215038.0-sha.986f9022 (model_invocation_service.ts identical).
  • macOS, local execution, filesystem datastore.
02Bog Flow
✓OPEN✓TRIAGED✓IN PROGRESS✓SHIPPED+ 1 MOREASSIGNED+ 14 MOREREVIEW+ 7 MOREPR_MERGED+ 2 MORESESSION_SUMMARIZED

Shipped

10/11/2026, 2:36:23 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack7210/11/2026, 12:36:02 AM
Editable. Press Enter to edit.

stack72 commented 10/11/2026, 2:36:26 PM

Thanks @dmc for reporting this! We shipped: Make context.runModel resolve the target definition's CEL and vault/env expressions before execution, as swamp model method run and workflow steps do, using an injected ExpressionEvaluationService, a fresh per-invocation secret bag and the caller's redactor. Caller global arguments holding template text that reads any swamp root (or fails to parse) are refused in every mode; template-free globals are applied before evaluation (reaching the method and self.globalArguments), and remaining foreign template text and method arguments are applied after both passes, so caller text is never evaluated.. The fix has been merged and a release is on its way. We appreciate your contribution to swamp.

Sign in to post a ripple.