Agent Shell
A virtual bash environment with a persistent filesystem backed by Tigris object storage, written in TypeScript and designed for AI agents.
AI agents produce artifacts — reports, data, configs, logs. These need to go
somewhere durable, shareable, and globally accessible. Agent Shell gives agents
a familiar bash interface (cat, grep, sed, jq, awk, pipes, redirects)
where every file operation is backed by a Tigris bucket.
- Isolated — writes stay in-memory until you explicitly flush. No partial state leaks to storage.
- Durable — flush persists files to Tigris, globally distributed.
- Checkpointable — take snapshots of your storage at any point. Roll back if needed.
- Forkable — create copy-on-write forks of a bucket for safe experimentation.
- Shareable — generate presigned URLs for any stored file.
Built on just-bash for the shell engine and @tigrisdata/storage for the storage layer.
Getting Started
Installation
- NPM
- Yarn
npm install @tigrisdata/agent-shell
yarn add @tigrisdata/agent-shell
Programmatic Usage
import { TigrisShell } from "@tigrisdata/agent-shell";
const shell = new TigrisShell({
accessKeyId: process.env.TIGRIS_STORAGE_ACCESS_KEY_ID,
secretAccessKey: process.env.TIGRIS_STORAGE_SECRET_ACCESS_KEY,
bucket: process.env.TIGRIS_STORAGE_BUCKET, // optional — auto-mounts at cwd (default: /workspace)
});
await shell.exec('echo "Hello from agent-shell" > greeting.txt');
await shell.exec("cat greeting.txt | tr a-z A-Z");
// stdout: "HELLO FROM AGENT-SHELL"
// Persist changes to Tigris
await shell.flush();
Interactive Shell
Launch a shell directly — no install needed:
npx @tigrisdata/agent-shell
Authenticate with access keys:
$ configure --key tid_... --secret tsec_...
Mounted 2 bucket(s) at /. Run 'df' to list them.
/ $ ls
my-bucket shared-data
/ $ cd my-bucket
/my-bucket $ echo "hello" > greeting.txt
/my-bucket $ cat greeting.txt
hello
/my-bucket $ flush
Flushed 2 mount(s)
Or login with your Tigris account:
$ login
Open this URL in your browser:
https://auth.storage.tigrisdata.io/activate?user_code=XKCD-1234
Waiting for authorization... done!
Logged in as you@example.com
Mounted 2 bucket(s) at /. Run 'df' to list them.
You can also pass credentials as flags:
npx @tigrisdata/agent-shell --key tid_... --secret tsec_... --bucket my-bucket
Storage Model
Agent Shell uses an in-memory write-back cache that provides isolation:
Agent writes file → cached in memory (isolated)
Agent reads file → cache hit or fetch from Tigris
Agent calls flush → all changes persisted atomically
This gives you:
- Isolation — nothing touches storage until you say so
- Atomic commits — if the agent fails midway, no partial state is written
- Fast execution — most operations never hit the network
const shell = new TigrisShell({
accessKeyId: process.env.TIGRIS_STORAGE_ACCESS_KEY_ID,
secretAccessKey: process.env.TIGRIS_STORAGE_SECRET_ACCESS_KEY,
bucket: process.env.TIGRIS_STORAGE_BUCKET,
});
try {
await shell.exec('echo "processing..." > status.txt');
await shell.exec("echo '{\"score\": 0.95}' > results.json");
await shell.exec("cat results.json | jq .score"); // "0.95\n"
// Only persist on success
await shell.flush();
} catch (e) {
// Nothing was written to Tigris — storage is clean
}
Configuration
TigrisConfig
| Option | Type | Required | Description |
|---|---|---|---|
bucket | string | No | Bucket name — auto-mounted at cwd |
accessKeyId | string | No* | Tigris access key ID |
secretAccessKey | string | No* | Tigris secret access key |
sessionToken | string | No* | Session token (from OAuth) |
organizationId | string | No* | Organization ID (required with token) |
endpoint | string | No | Tigris endpoint URL |
At least one auth mode is required: (accessKeyId + secretAccessKey) or (sessionToken + organizationId).
ShellOptions
| Option | Type | Default | Description |
|---|---|---|---|
cwd | string | /workspace | Starting working directory and auto-mount point |
env | Record<string, string> | {} | Initial environment variables for the shell |
const shell = new TigrisShell(
{
accessKeyId: process.env.TIGRIS_STORAGE_ACCESS_KEY_ID,
secretAccessKey: process.env.TIGRIS_STORAGE_SECRET_ACCESS_KEY,
bucket: process.env.TIGRIS_STORAGE_BUCKET,
},
{ cwd: "/my-project", env: { NODE_ENV: "production" } },
);
API Reference
TigrisShell
| Method / Property | Type | Description |
|---|---|---|
constructor | (config: TigrisConfig, options?: ShellOptions) | Create a new shell instance |
exec(command) | Promise<BashExecResult> | Execute a bash command |
mount(bucket, mountPoint) | void | Mount a bucket at a path |
unmount(mountPoint) | void | Unmount a path |
listMounts() | Array<{ bucket, mountPoint }> | List current mounts |
flush(mountPoint?) | Promise<void> | Flush all mounts or a specific one |
engine | Bash | Underlying just-bash instance |
Sub-exports
Agent Shell exposes additional modules for advanced use cases:
// Filesystem adapter for custom storage layouts
import { TigrisAdapter } from "@tigrisdata/agent-shell/fs";
// Individual Tigris command factories
import {
createTigrisCommands,
createPresignCommand,
createSnapshotCommand,
createForkCommand,
} from "@tigrisdata/agent-shell/commands";
Built-in Tigris Commands
Beyond standard bash commands, Agent Shell includes Tigris-specific commands.
presign
Generate presigned URLs for sharing or uploading:
presign /path/to/file.txt # GET URL, 1 hour expiry
presign /path/to/file.txt --expires 7200 # GET URL, 2 hour expiry
presign /path/to/file.txt --put # PUT URL for uploads
snapshot
Checkpoint your storage. Create or list point-in-time bucket snapshots:
snapshot my-bucket # Create a snapshot
snapshot my-bucket --name "checkpoint-1" # Create a named snapshot
snapshot my-bucket --list # List all snapshots
fork
Branch your storage. Create a copy-on-write fork for safe experimentation:
fork source-bucket my-fork # Fork a bucket
fork source-bucket my-fork --snapshot 1713200000 # Fork from a specific snapshot
forks
List forks of a bucket:
forks my-bucket
Multi-Bucket
Mount multiple buckets at different paths:
import { TigrisShell } from "@tigrisdata/agent-shell";
const shell = new TigrisShell({
accessKeyId: process.env.TIGRIS_STORAGE_ACCESS_KEY_ID,
secretAccessKey: process.env.TIGRIS_STORAGE_SECRET_ACCESS_KEY,
bucket: "agent-workspace", // auto-mounted at cwd (default: /workspace)
});
shell.mount("shared-datasets", "/datasets");
await shell.exec("cat /datasets/training/labels.csv | head -10");
await shell.exec("cp /datasets/training/labels.csv ./local-copy.csv");
await shell.exec('echo "processed" > results.txt');
// Flush all mounts, or a specific one
await shell.flush(); // all
await shell.flush("/datasets"); // just /datasets
// List and unmount
shell.listMounts();
shell.unmount("/datasets");
Or in the interactive shell:
$ mount shared-datasets /datasets
Mounted shared-datasets at /datasets
$ cp /datasets/data.csv ./local.csv
$ df
Bucket Mounted on
agent-workspace /agent-workspace
shared-datasets /datasets
$ umount /datasets
Unmounted /datasets
Examples
Processing Files
import { TigrisShell } from "@tigrisdata/agent-shell";
const shell = new TigrisShell({
bucket: process.env.TIGRIS_STORAGE_BUCKET,
accessKeyId: process.env.TIGRIS_STORAGE_ACCESS_KEY_ID,
secretAccessKey: process.env.TIGRIS_STORAGE_SECRET_ACCESS_KEY,
});
// Read a file already in the bucket
const result = await shell.exec("cat data.csv");
console.log(result.stdout);
// Process with standard Unix tools
const sorted = await shell.exec("cat data.csv | tail -n +2 | sort -t, -k2 -rn");
console.log(sorted.stdout);
// Write results and persist
await shell.exec("cat data.csv | wc -l > stats.txt");
await shell.flush();
Snapshots, Forks, and Presigned URLs
import { TigrisShell } from "@tigrisdata/agent-shell";
const shell = new TigrisShell({
bucket: process.env.TIGRIS_STORAGE_BUCKET,
accessKeyId: process.env.TIGRIS_STORAGE_ACCESS_KEY_ID,
secretAccessKey: process.env.TIGRIS_STORAGE_SECRET_ACCESS_KEY,
});
// Write a file and persist
await shell.exec('echo "quarterly report" > report.txt');
await shell.flush();
// Generate a shareable URL
const getUrl = await shell.exec("presign report.txt");
console.log(getUrl.stdout.trim());
// Checkpoint storage before changes
await shell.exec("snapshot my-bucket --name before-migration");
// Branch storage for safe experimentation
await shell.exec("fork my-bucket my-bucket-experiment");