From a7d964c84f23a460e9be6693e7dcdd1f9c894094 Mon Sep 17 00:00:00 2001 From: stack72 Date: Wed, 6 May 2026 17:14:58 +0100 Subject: [PATCH] fix(drivers): resolve vault sentinels before dispatching to out-of-process drivers (swamp-club#263) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The method execution service built an ExecutionRequest with unresolved vault sentinel tokens in methodArgs and globalArgs. The raw driver resolves sentinels internally, but out-of-process drivers (docker, custom) received the sentinels as-is — delivering literal __SWAMP_VSEC_*__ strings to the model instead of decrypted values. Call secretBag.resolveDeep() on the execution request's args in the non-raw driver dispatch path before invoking the driver. The resolution operates on structuredClone data from the definition, so persisted state is never mutated. Co-Authored-By: Claude Opus 4.6 (1M context) --- design/execution-drivers.md | 22 +++++- src/domain/models/method_execution_service.ts | 13 ++++ .../models/method_execution_service_test.ts | 71 +++++++++++++++++++ 3 files changed, 105 insertions(+), 1 deletion(-) diff --git a/design/execution-drivers.md b/design/execution-drivers.md index 920e5b1c..aa7c565f 100644 --- a/design/execution-drivers.md +++ b/design/execution-drivers.md @@ -258,7 +258,8 @@ HOST CONTAINER 1. Build ExecutionRequest ├─ globalArgs, methodArgs ├─ definitionMeta - └─ bundle (Uint8Array) + ├─ bundle (Uint8Array) + └─ Resolve vault sentinels in args 2. Create temp dir ├─ bundle.js ──mount──▶ /swamp/bundle.js @@ -284,6 +285,25 @@ HOST CONTAINER via DataWriter ``` +## Vault Secret Resolution + +Vault expressions (`${{ vault.get(...) }}`) produce sentinel tokens during +runtime expression resolution. These sentinels must be replaced with actual +secret values before execution. Each driver path handles this differently: + +- **Raw driver**: The `DefaultMethodExecutionService.execute()` method calls + `secretBag.resolveDeep()` on method args and global args before invoking the + model's `execute` function. The shell model additionally resolves sentinels + via environment variables (`resolveForShell`) to prevent shell injection. + +- **Out-of-process drivers** (docker, custom): The method execution service + resolves sentinels in `executionRequest.methodArgs` and + `executionRequest.globalArgs` before dispatching to the driver. This ensures + drivers receive plaintext values without needing vault awareness. The + resolution operates on cloned data — the original definition is never mutated, + so sentinel tokens remain in the persisted definition while only the in-flight + request carries resolved values. + ## Output Parity Both drivers produce `DriverOutput[]` but with different `kind` values: diff --git a/src/domain/models/method_execution_service.ts b/src/domain/models/method_execution_service.ts index f669a1a0..1116ff69 100644 --- a/src/domain/models/method_execution_service.ts +++ b/src/domain/models/method_execution_service.ts @@ -709,6 +709,19 @@ export class DefaultMethodExecutionService implements MethodExecutionService { ); } + // Resolve vault sentinels for out-of-process drivers. The raw driver + // resolves sentinels internally via DefaultMethodExecutionService.execute(), + // but out-of-process drivers receive the ExecutionRequest as-is. + const secretBag = context.vaultSecrets; + if (secretBag && !secretBag.isEmpty) { + executionRequest.methodArgs = secretBag.resolveDeep( + executionRequest.methodArgs, + ) as Record; + executionRequest.globalArgs = secretBag.resolveDeep( + executionRequest.globalArgs, + ) as Record; + } + // Look up a registered driver type await driverTypeRegistry.ensureTypeLoaded(driverType); const driverInfo = driverTypeRegistry.get(driverType); diff --git a/src/domain/models/method_execution_service_test.ts b/src/domain/models/method_execution_service_test.ts index cb413eda..3d764e22 100644 --- a/src/domain/models/method_execution_service_test.ts +++ b/src/domain/models/method_execution_service_test.ts @@ -36,6 +36,11 @@ import { Data } from "../data/data.ts"; import { UserError } from "../errors.ts"; import { getLogger } from "@logtape/logtape"; import { VaultSecretBag } from "../vaults/vault_secret_bag.ts"; +import type { + ExecutionRequest, + ExecutionResult, +} from "../drivers/execution_driver.ts"; +import { driverTypeRegistry } from "../drivers/driver_type_registry.ts"; /** * Test model that mimics the echo model's write method. @@ -2819,3 +2824,69 @@ Deno.test("executeWorkflow - rejects unknown global arg key", async () => { "Global arguments validation failed", ); }); + +Deno.test( + "executeWorkflow: out-of-process driver receives resolved vault secrets, not sentinels", + async () => { + const service = new DefaultMethodExecutionService(); + + const driverType = `test-capture-${crypto.randomUUID().slice(0, 8)}`; + let capturedRequest: ExecutionRequest | null = null; + + driverTypeRegistry.register({ + type: driverType, + name: "Test capture driver", + description: "Captures ExecutionRequest for assertions", + isBuiltIn: false, + createDriver: () => ({ + type: driverType, + execute: (request: ExecutionRequest): Promise => { + capturedRequest = request; + return Promise.resolve({ + status: "success", + outputs: [], + logs: [], + durationMs: 0, + }); + }, + }), + }); + + const model: ModelDefinition = { + type: ModelType.create("test/driver-vault"), + version: "1.0.0", + globalArguments: z.object({ apiKey: z.string() }), + resources: {}, + methods: { + run: { + description: "Test method", + arguments: z.object({ token: z.string() }), + execute: () => Promise.resolve({ dataHandles: [] }), + }, + }, + }; + + const secretBag = new VaultSecretBag(); + const apiKeySentinel = secretBag.addSecret("real-api-key-value"); + const tokenSentinel = secretBag.addSecret("real-token-value"); + + const definition = Definition.create({ + name: "test-driver-vault-def", + type: "test/driver-vault", + globalArguments: { apiKey: apiKeySentinel }, + methods: { run: { arguments: { token: tokenSentinel } } }, + }); + + const { context } = createTestContext({ + modelType: model.type, + vaultSecrets: secretBag, + driver: driverType, + }); + + await service.executeWorkflow(definition, model, "run", context); + + assertEquals(capturedRequest !== null, true); + assertEquals(capturedRequest!.globalArgs.apiKey, "real-api-key-value"); + assertEquals(capturedRequest!.methodArgs.token, "real-token-value"); + }, +);