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

#263 Vault expressions silently deliver __SWAMP_VSEC__ sentinels under the docker driver

Opened by john · 5/6/2026· Shipped 5/6/2026

What's broken

Vault expressions in step inputs: advertise per-step runtime resolution — swamp-vault skill: "Vault expressions are resolved per-step at execution time — each step gets a fresh vault read." This holds under the raw driver, but the docker driver delivers the raw __SWAMP_VSEC_<hash>_<n>__ sentinel string to the model instead of the decrypted value, with no error and no warning.

Concrete symptom: a step that does claudeCredentials: \${{ vault.get(my-vault, my-key) }} and writes the value to disk inside the container ends up persisting a 25-byte file containing literally __SWAMP_VSEC_b0a22fa1_0__. The model has no signal that resolution failed.

Reproduce

  1. Create any vault that returns a non-empty string (we hit it with @webframp/macos-keychain, but it's not vault-type-specific — it's about how the docker driver moves args).
  2. Author an extension model with a string arg, and have its execute write that arg to a file.
  3. Set the model up with the docker driver.
  4. In a workflow step, set inputs.<argName>: \${{ vault.get(<vault>, <key>) }}.
  5. Run the workflow. The file on disk inside the container will contain __SWAMP_VSEC_<hash>_<n>__, not the secret.

Removing z.meta({ sensitive: true }) from the arg's zod schema makes no difference — the sentinel is generated upstream of the schema.

Where the gap lives (from a quick code dive)

  • Sentinel generator: src/domain/vaults/vault_secret_bag.ts:70 (VaultSecretBag.addSecret()), called unconditionally from ModelResolver.resolveVaultExpressions() at src/domain/expressions/model_resolver.ts:856.
  • Resolver paths used elsewhere: resolveRaw, resolveDeep, resolveForShell at vault_secret_bag.ts:84–143.
  • Raw-driver method execution resolves them: DefaultMethodExecutionService calls secretBag.resolveDeep(methodArgs) at line ~248 before invoking the model's execute.
  • Shell model resolves via env-var injection: domain/models/command/shell/shell_model.ts:109 calls resolveForShell().
  • Docker driver: src/domain/drivers/docker_execution_driver.ts:529–532 accepts driverConfig.env and method args, but does not call resolveDeep() or resolveForShell() on either. methodArgs are serialised into the bundle request payload with sentinels intact; env values are passed via -e KEY=VALUE literally.

Why this is a bug, not a docs gap

Two reasons:

  1. The contract is advertised as driver-agnostic. The swamp-vault skill describes per-step resolution as a property of vault expressions, with no caveat about the docker driver. The principle of least surprise says either it works or it errors loudly.
  2. The current behaviour is silent data corruption from the model author's perspective. The model receives a string that's syntactically valid for its schema but semantically wrong. There's no distinguishable failure mode — the model just operates on garbage and produces nonsense outputs. We hit this and only diagnosed it by hand-inspecting an on-disk artefact.

Suggested fix

The docker driver should resolve sentinels before invoking docker, in one of two ways:

  • Inline-substitute into method args (mirroring resolveDeep's effect for raw): the JSON payload sent into the container already contains plaintext. Simple, but exposes secrets in the bundle request file on disk.
  • Forward as env vars (mirroring resolveForShell's pattern): pass -e __SWAMP_VAULT_N=value for each sentinel, and ship a small replacement table to the model's bundle entrypoint that swaps __SWAMP_VSEC_<...>__ placeholders in method args back to \$__SWAMP_VAULT_N env reads at the moment of execute. Keeps the secret out of the request file on disk.

Either is a clear improvement over today's silent passthrough.

Workaround (currently)

Add a raw-driver pre-step that does the vault read and writes the secret to a host path that the docker step mounts. Works, but: (a) requires every workflow author to know about this gap; (b) writes the secret to the host filesystem unconditionally; (c) breaks the declarative "just put the vault expression where you need the value" model swamp otherwise sells very well.

Environment

  • swamp 20260206.200442.0-sha. (CLI) / 20260505.231643.0-sha.5a337b81 (vault subcommands)
  • macOS host, Debian-based docker image (denoland/deno:debian-2.4.0)
  • Vault provider: @webframp/macos-keychain 2026.04.22.3 (but the issue is in the driver, not the vault)
02Bog Flow
OPENTRIAGEDIN PROGRESSSHIPPED+ 1 MOREASSIGNED+ 2 MOREREVIEW+ 3 MOREPR_MERGEDSHIPPED

Shipped

5/6/2026, 4:29:37 PM

Click a lifecycle step above to view its details.

03Sludge Pulse
stack72 assigned stack725/6/2026, 3:26:13 PM

Sign in to post a ripple.