Start with a Household Analogy
Think about a light switch. It doesn't care where the electricity comes from — the power grid, a solar panel, or a generator all work fine. The switch only defines one thing: on/off. As long as whatever is behind it honors that interface, you flip it and the light comes on.
That's the essence of a Seam: an interface boundary that separates "using this capability" from "how this capability is implemented."
In dsh:
-
Filesystem operations are a Seam (
ctx.fs) -
Shell execution is a Seam (
ctx.shell) -
LLM calls are a Seam (
ctx.llm) -
Process sandboxing is a Seam (
ctx.sandbox)
You can swap the default local filesystem for an E2B sandbox filesystem without changing a single line of tool code. That's the problem Capability Seams solve.
Three Roles in Every Seam
Every Seam involves three participants:
┌────────────────────────────────────────────────────────┐
│ Consumer: tool-fs │
│ import ctx.fs → calls ctx.fs.readFile(path) │
│ ctx.fs.writeFile(path, content) │
└─────────────────────┬──────────────────────────────────┘
│ depends on service "ctx.fs"
┌─────────────────────▼──────────────────────────────────┐
│ Service Definition: dsh-fs │
│ interface FileSystem { readFile, writeFile, ... } │
└─────────────────────┬──────────────────────────────────┘
│ implements
┌───────────────┴───────────────┐
▼ ▼
fs-local fs-e2b
(local filesystem) (E2B sandbox)
The division of responsibilities:
| Role | Responsibility | Example |
|---|---|---|
| Service Definition | Define the interface + service name |
dsh-fs: defines the method signatures of ctx.fs
|
| Service Provider | Implement the interface, register it on ctx
|
fs-local, fs-e2b, fs-sandbox
|
| Service Consumer | Use the interface, unaware of the implementation |
tool-fs: the file-reading/writing tool plugin |
A Consumer only knows the service name (like ctx.fs) — it has no idea which Provider is behind it. Swapping a Provider only touches the Bundle configuration; Consumers are completely unaffected.
Core Seams at a Glance
dsh ships with the following built-in Seams:
| Seam | Service Name | Default Implementation | Can Be Swapped For |
|---|---|---|---|
| Filesystem | ctx.fs |
fs-local (host machine) |
fs-e2b (E2B sandbox), fs-sandbox (restricted local) |
| Shell execution | ctx.shell |
bash-local |
bash-sandbox (restricted exec), remote shell |
| Process sandbox | ctx.sandbox |
sandbox-local (bwrap/Seatbelt/ACL) |
container, microVM |
| LLM adapter | ctx.llm |
llm-deepseek |
llm-pi-ai and other third-party providers |
| Auth credentials | ctx.credentials |
credentials-local |
remote vault |
| User settings | ctx.settings |
settings-file |
remote settings service |
| Persistent storage | ctx.sessionPersistence |
session-persistence-jsonl |
SQLite, remote backend |
Deep Dive: ctx.fs (Filesystem Seam)
Why not just import fs from 'node:fs'?
Directly importing Node.js's built-in fs module creates three concrete problems:
-
During testing: to test any "read a file" logic, you need real files on disk. Or you mock
fs, but mocking Node built-ins requires extra tooling (jest.mock, etc.) and is a headache. -
Inside a sandbox: E2B has its own filesystem API — it's not Node.js
fs. If a tool directly importsnode:fs, it simply won't run in E2B. - Auditing: you can't uniformly intercept all file reads and writes (for logging, permission checks) because the call sites are scattered across every tool.
With the ctx.fs Seam, all three problems disappear:
- In tests: inject an in-memory filesystem Provider
- In E2B: inject the
fs-e2bProvider - For auditing: log all calls uniformly in the Provider layer
Consumer code (conceptual example)
// Tool plugin: read a file — only depends on ctx.fs, doesn't care about the implementation
// Declare dependency on the 'fs' service
export const inject = ['fs']
export function apply(ctx: Context): void {
ctx.tools.register(defineTool({
name: 'read_file',
description: 'Read the contents of a file',
execute: async (args, exec) => {
// ctx.fs is the filesystem Seam — local, sandbox, or E2B,
// depending on which Provider was loaded
// This code has no idea what's backing it
const content = await ctx.fs.readFile(args.path)
return content
},
}))
}
// Want to switch to a sandboxed filesystem?
// Just swap the Provider plugin in the Bundle:
// bundle.ts (conceptual example)
export default [
// Swapping this one line is all it takes — tool code is untouched:
// '@deepseek-ai/dsh-fs-local' → local filesystem (default)
// '@deepseek-ai/dsh-fs-sandbox' → restricted local filesystem
// '@deepseek-ai/dsh-fs-e2b' → E2B sandbox filesystem
'@deepseek-ai/dsh-fs-sandbox', // ← only this line changes
// Consumer is unchanged
'@deepseek-ai/dsh-tool-fs',
// ... other plugins
]
Deep Dive: ctx.sandbox (Process Sandbox Seam)
Shell execution is backed by the process sandbox. dsh defines three sandbox modes:
// Three sandbox modes for process execution (conceptual example)
type SandboxMode =
| 'read-only' // Read-only: child processes cannot write any files
| 'workspace-write' // Workspace-write: can only write inside the working directory
| 'danger-full-access' // Unrestricted: no sandbox, direct spawn
The platform implementation is selected automatically:
| Platform | Underlying Mechanism |
|---|---|
| Linux | bwrap + Landlock |
| macOS | Seatbelt |
| Windows | ACL-restricted token |
Switching backends with one config line (conceptual example):
# Local development: sandbox-local (native platform sandbox)
# CI/CD: sandbox-e2b (E2B container, fully isolated — a sandbox crash won't touch the host)
The same tool-shell code runs with sandbox-local locally and sandbox-e2b in CI. The switch lives entirely in the Bundle configuration layer.
Deep Dive: ctx.llm (LLM Adapter Seam)
LLMs are Seams too. This surprises people at first, but it makes perfect sense once you think it through.
Why abstract LLM calls?
- Provider switching: same Agent code, lightweight model for local testing, DeepSeek in production — swap one Provider plugin.
-
Replay testing:
llm-replayProvider — no real API calls, just plays backassistant/messageevents from a historical session in order. Testing Agent behavior requires no API key and has zero randomness. - Multi-model routing: implement a Provider that routes different tasks to different models.
// llm-replay use case (conceptual example)
// In tests:
// 1. Run one real conversation and save the session log
// 2. In subsequent tests, swap the Provider to llm-replay —
// the replay adapter plays back historical assistant/message events in order
// 3. Tests are fully deterministic, consume no API quota, and run 10x faster
// How to switch — one line in Bundle:
// '@deepseek-ai/dsh-llm-deepseek' → real API
// '@deepseek-ai/dsh-llm-replay' → replay mode (for testing)
Hands-On: Implementing Your Own Provider
Here's a read-only filesystem Provider — all write operations throw immediately. Useful when you want to give an Agent read access to a codebase but not allow any modifications.
// Read-only filesystem Provider (conceptual example)
// Use case: let an Agent read a codebase, but never modify any files
export const name = 'my-readonly-fs'
export function apply(ctx: Context): void {
// Register as the Provider for the ctx.fs service
ctx.provide('fs', {
// Read operations: work normally
async readFile(path: string): Promise<string> {
return await localReadFile(path)
},
// Write operations: intercepted, throw a clear error
async writeFile(path: string, content: string): Promise<void> {
throw new Error(`Read-only filesystem: cannot write to ${path}`)
},
// Directory listing: works normally
async listFiles(dir: string): Promise<string[]> {
return await localListFiles(dir)
},
// ... other methods (mkdir, rm, etc. all throw the read-only error)
})
}
// Using the custom Provider in a Bundle (conceptual example)
export default [
// Replace the default fs-local with your read-only Provider
'./my-readonly-fs',
// tool-fs is unchanged — it doesn't know the filesystem is read-only
'@deepseek-ai/dsh-tool-fs',
]
The power of this pattern: you've restricted the Agent's filesystem permissions without touching a single line of tool-fs.
The Real Value of Seam Design
A side-by-side comparison:
| Scenario | Without Seam | With Seam |
|---|---|---|
| Switch LLM provider | Update every API call site in every tool | Swap one Provider plugin |
| Unit test file operations | Mock node:fs, or create real files on disk |
Inject an in-memory filesystem Provider |
| Deploy to a sandbox environment | Adapt large amounts of tool code to the sandbox API | Swap fs-sandbox or fs-e2b
|
| Add a new platform/backend | Modify core code, risk introducing bugs | Implement a new Provider; Consumers are unaffected |
| Enforce permissions | Add if-guards in every individual tool | Intercept uniformly in the Provider layer |
Capability Seams are essentially a specialized form of dependency injection (DI). In the plugin system, injection is keyed by service name rather than constructor parameter — but the principle is identical.
Design Principles Summary
The core idea behind dsh Capability Seams:
Consumers only know the interface name. Providers only know the interface. Swapping happens at the configuration layer.
| Design Decision | Reason |
|---|---|
| All external capabilities go through Seams | Testing, deployment, and multi-backend support all benefit |
| LLM is also a Seam | Enables replay testing, provider switching, multi-model routing |
| Sandbox is also a Seam | Local/CI/remote environments use different sandbox backends; Agent code is unchanged |
| Consumers only declare a service name | Complete decoupling — no dependency on any concrete implementation |
| Bundle is the configuration layer | Composing different Providers is a one-place change; business code stays clean |
What's Next in the Series
The next article covers multi-Agent collaboration: when a single Agent can't handle a complex task, how does dsh support multiple Agents dividing and coordinating the work — delegation, subagents, parallel execution, and how all of this is represented at the Session layer.
Check out PrimeSkills — a curated marketplace of AI agents and skills validated in real-world, enterprise-grade workflows. Not demos — things that actually work in production.
Find more on my Homepage
Top comments (0)