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
- 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>) }}
swamp model method run node-a listVmSnapshots --input vmid:json=200→ works.- 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'sVaultSecretBagis 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 tobuildMethodContext(… globalArgs: mergedGlobalArgs …)andexecutionService.executeWorkflow(childDefinition, …). No expression evaluation or vault resolution happens on this path.resolveRuntimeExpressionsInDefinitionis called only fromsrc/libswamp/models/run.ts(CLImodel method run) andsrc/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.tsidentical). - macOS, local execution, filesystem datastore.
Shipped
Click a lifecycle step above to view its details.
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.