feat: Execution Driver Abstraction — Pluggable Isolation for Model Execution
Opened by swampadmin · 9/13/2025
Summary
Swamp currently has no execution isolation — all model methods run directly in the host Deno process ("raw exec"). This issue proposes introducing pluggable execution drivers so users can opt-in to isolated execution environments (starting with Docker), following the same pattern as the existing VaultProvider/VaultTypeRegistry system.
Key constraint: Zero breaking changes. If no driver field is specified, the system defaults to "raw" and behaves identically to today.
Motivation
- Security (SA-01): Extension models execute with full process privileges. A malicious or compromised extension can exfiltrate vault secrets, read/write arbitrary files, spawn subprocesses, and make unrestricted network requests. The driver abstraction creates the architectural boundary needed for runtime isolation.
- Extensibility: Users should be able to define custom execution drivers (Docker, Lambda, SSH, Deno Worker) via
extensions/drivers/, just like they can define custom vault providers today. - Consistency: Follows the same proven pattern as
VaultProvider/VaultTypeRegistry.
Design
Driver Interface
interface ExecutionDriver {
readonly type: string;
execute(request: ExecutionRequest, callbacks: ExecutionCallbacks): Promise<ExecutionResult>;
initialize?(): Promise<void>;
shutdown?(): Promise<void>;
}ExecutionRequest— Serializable envelope containing model type, method name, pre-evaluated arguments, output specs, and optionally the bundled model code.ExecutionResult— Status, outputs (persisted or pending), logs, duration.DriverOutput— Discriminated union:{ kind: "persisted", handle: DataHandle }(raw driver writes directly) |{ kind: "pending", content: Uint8Array, ... }(out-of-process drivers return content for host-side persistence).
Built-in Drivers
raw(default) — Current behavior. Runs in-process with full access. Zero overhead.docker(opt-in) — Serializes context, runs in a container, collects results. Vault secrets piped via stdin (never on disk).
User-Defined Drivers
Loaded from extensions/drivers/ using the same bundle→import→register flow as vault providers:
// extensions/drivers/lambda-driver.ts
export const driver = {
type: "@acme/lambda",
name: "AWS Lambda",
description: "Execute methods as Lambda functions",
configSchema: z.object({ functionName: z.string(), region: z.string().default("us-east-1") }),
createDriver(config) {
return { type: "@acme/lambda", async execute(request, callbacks) { /* ... */ } };
},
};Driver Configuration in YAML
Optional driver and driverConfig fields at multiple levels with precedence:
step > job > workflow > model definition > "raw"Model definition level:
name: my-vpc
type: aws/ec2/vpc
driver: docker
driverConfig:
memory: 512m
network: noneWorkflow step level (overrides model):
jobs:
deploy:
steps:
- name: create-vpc
driver: docker
driverConfig:
memory: 1g
task:
model_method:
model: my-vpc
method: createWorkflow/job level (inherited by all steps):
driver: docker
driverConfig:
image: custom-runner:latest
jobs:
deploy:
steps:
- name: create-vpc
task: ...CLI flags:
swamp model method run my-vpc create --driver docker
swamp workflow run deploy --driver dockerImplementation Plan
Phase 1: Domain Interfaces and Registry (no behavioral changes)
Create src/domain/drivers/ with:
execution_driver.ts— Core interfaces (ExecutionDriver,ExecutionRequest,ExecutionResult,DriverOutput)driver_type_registry.ts—DriverTypeRegistrysingleton (mirror ofVaultTypeRegistry)driver_types.ts— Built-in"raw"and"docker"registration on module loadraw_execution_driver.ts— Stub (fleshed out in Phase 3)docker_execution_driver.ts— Stubmod.ts— Barrel re-export- Tests for registry and interface conformance
Phase 2: Schema Changes (all optional, zero breakage)
Add optional driver and driverConfig fields to:
DefinitionSchema+Definitionclass (src/domain/definitions/definition.ts)WorkflowSchema+Workflowclass (src/domain/workflows/workflow.ts)JobSchema+Jobclass (src/domain/workflows/job.ts)StepSchema+Stepclass (src/domain/workflows/step.ts)
Create driver_resolution.ts with precedence resolution function.
Phase 3: Wire RawExecutionDriver
Extract lines 328-379 of DefaultMethodExecutionService.executeWorkflow() (create writers → inject context → call method.execute()) into RawExecutionDriver.execute(). The orchestration layer delegates to the resolved driver. Defaults to "raw" — identical behavior.
Key design: RawExecutionDriver is constructed per-execution with in-process references (MethodDefinition, MethodContext). Out-of-process drivers use the serialized ExecutionRequest.
What stays in orchestration: validation, lifecycle checks, ModelOutput tracking, deletion markers, follow-up actions.
Phase 4: User-Defined Driver Loader
Create UserDriverLoader (mirror of UserVaultLoader): discover .ts files in extensions/drivers/, bundle, import, validate, register with driverTypeRegistry.
Wire into src/cli/mod.ts startup alongside loadUserModels() and loadUserVaults().
Phase 5: Wire Driver Resolution Through Execution Path
Connect driver/driverConfig from YAML through StepExecutionContext → MethodContext → MethodExecutionService. Add --driver CLI flag to both:
swamp model method run(src/cli/commands/model_method_run.ts)swamp workflow run(src/cli/commands/workflow_run.ts)
The workflow CLI flag flows into the execution service as the workflow-level driver, so step/job-level overrides in YAML still take precedence.
Phase 6: DockerExecutionDriver (future PR)
Serialize ExecutionRequest to JSON, pipe via stdin to docker run, stream stderr for real-time logs, parse ExecutionResult from stdout, return kind: "pending" outputs for host-side persistence. Runner image: minimal Deno container.
Files Summary
New files
| File | Phase |
|---|---|
src/domain/drivers/execution_driver.ts |
1 |
src/domain/drivers/driver_type_registry.ts |
1 |
src/domain/drivers/driver_types.ts |
1 |
src/domain/drivers/raw_execution_driver.ts |
1+3 |
src/domain/drivers/docker_execution_driver.ts |
1 |
src/domain/drivers/mod.ts |
1 |
src/domain/drivers/driver_config.ts |
2 |
src/domain/drivers/driver_resolution.ts |
2 |
src/domain/drivers/user_driver_loader.ts |
4 |
Modified files
| File | Phase | Change |
|---|---|---|
src/domain/definitions/definition.ts |
2 | Add driver/driverConfig to schema + class |
src/domain/workflows/workflow.ts |
2 | Add driver/driverConfig to schema + class |
src/domain/workflows/job.ts |
2 | Add driver/driverConfig to schema + class |
src/domain/workflows/step.ts |
2 | Add driver/driverConfig to schema + class |
src/domain/models/model.ts |
3 | Add driver/driverConfig to MethodContext |
src/domain/models/method_execution_service.ts |
3 | Refactor to delegate to driver |
src/infrastructure/persistence/paths.ts |
4 | Add driverBundles to SWAMP_SUBDIRS |
src/cli/mod.ts |
4 | Add loadUserDrivers(), import driver barrel |
src/domain/workflows/execution_service.ts |
5 | Pass driver config through execution context |
src/cli/commands/model_method_run.ts |
5 | Pass definition driver + --driver CLI flag |
src/cli/commands/workflow_run.ts |
5 | Add --driver CLI flag, pass to execution service |
Security Context
This work directly addresses SA-01: Extension Models Execute Without Runtime Sandbox. The driver abstraction creates the architectural boundary needed for:
- Docker driver: Full container isolation, configurable network/resource limits
- Future Deno Worker driver:
--allow-read=$REPO_DIR --deny-env --deny-run - Future subprocess driver: Restricted Deno permissions
- Vault secret containment: Non-raw drivers receive pre-resolved secrets via serialized request, never the
VaultServiceobject
The security policy layer (e.g., community extensions defaulting to isolated drivers, permission manifests) will be addressed in a follow-up issue.
Closed
No activity in this phase yet.
Sign in to post a ripple.