A virtual shell-over-database filesystem for AI agents: a strict, append-only memory layer with a frozen shell dialect.
Project description
virtual-shell
A virtual shell-over-database filesystem for AI agents.
virtual-shell gives an AI agent a private filesystem with a shell-style
interface (ls, cat, grep, echo) where the backend is a database, not
a disk. Agents are trained on millions of real terminal sessions, so a shell
is the interface they use most reliably — but a real shell executes arbitrary
code and a real disk needs per-agent compute. This package keeps the familiar
language and replaces everything underneath with a strict, parameterized,
append-only storage layer.
It is a memory layer only. It reads, writes, searches, and organizes files. It never executes programs, accesses the network, or spawns processes.
Status: early development (spec phase). The behavior is fully specified in
openspec/(change:add-virtual-shell) and being implemented incrementally across milestones M1–M4. This is a Python port of the v1.0 technical specification; the spec is the source of truth where the two disagree.
Documentation
- New here? Read the plain-language guide. — what it is, why, and how it's built, for dummies.
- Architecture snapshot — module map, data model, boundaries, extensibility seams.
- Technical specification v1.0 — the normative source of truth (open in a browser).
- OpenSpec change
add-virtual-shell— the proposal, design, capability specs, and step-by-step tasks underopenspec/changes/. Browse withopenspec show add-virtual-shell.
Why
- Phrasebook, not translator. A whitelist parser recognizes a frozen shell dialect and compiles it to a typed AST. There is no general shell interpretation and no path by which agent text reaches SQL as anything other than a bind parameter.
- Append-only. Every write is a new version row; deletes are tombstones. Nothing an agent does is destructive. Retention is a separate, operator-run compaction policy.
- Identity is injected. Tenant and agent IDs come from the host application; the dialect has no syntax for identity, and no syntax to escape a scope.
- Teaching errors. Every rejection tells the model what to do instead, so it self-corrects in one turn.
- Two doors, one table. Agents use the
Shell(strings in/out). Humans and UIs use theInspector(structured JSON, read-only). Same storage, same scope rules.
Install
pip install agent-virtual-shell # core, zero runtime dependencies
pip install "agent-virtual-shell[postgres]" # + asyncpg convenience extra
pip install "agent-virtual-shell[s3]" # + aioboto3 for the blob tier
Installs as
agent-virtual-shellon PyPI but imports asvirtual_shell(from virtual_shell import create_shell). The exact namevirtual-shellis blocked by PyPI's similarity guard, so the distribution carries theagent-prefix.
The Postgres adapter accepts any object satisfying a small async Queryable
protocol, so it works with asyncpg, a SQLAlchemy async session, or your own
wrapper — no hard dependency.
Quick start (planned API)
from virtual_shell import create_shell, Scope
from virtual_shell.adapters import MemoryAdapter
shell = create_shell(adapter=MemoryAdapter(), scope=Scope(tenant_id="t1", agent_id="a1"))
print((await shell.exec('echo "hello" > /notes/todo.md')).stdout)
# wrote 6 bytes to /notes/todo.md (v1)
print((await shell.exec("cat /notes/todo.md")).stdout)
# hello
shell.exec never raises: every input — including pathological ones — resolves
to a ShellResult with ok, exit_code, stdout, and a teaching stderr.
Architecture
agent (LLM) dashboard / human
| |
tool call: shell(cmd) host app's HTTP API
| |
Shell (string in/out) Inspector (read-only JSON)
tokenize → parse → interpret tree / read / history / changes
\ /
\ scope injected by host /
v v
StorageAdapter (interface)
MemoryAdapter — reference impl, tests/dev
PostgresAdapter — parameterized SQL, dedicated `virtual_shell` schema
TieredAdapter — wraps any adapter + BlobStore for large files
Database
Tables live in a dedicated Postgres schema (virtual_shell by default) in
whatever database you point the adapter at — including one shared with a host
application. Apply schema.sql once:
psql "$DATABASE_URL" -f schema.sql
Wiring up the PostgresAdapter
PostgresAdapter imports no database driver — it accepts any object satisfying
the async Queryable protocol (query(sql, params) -> rows, native $1, $2, …
placeholders). A wrapper is ~5 lines:
# asyncpg
import asyncpg
from virtual_shell.adapters import PostgresAdapter
pool = await asyncpg.create_pool(dsn)
class AsyncpgQueryable:
async def query(self, sql, params=()):
return [dict(r) for r in await pool.fetch(sql, *params)]
adapter = PostgresAdapter(AsyncpgQueryable()) # schema="virtual_shell", table="files"
# SQLAlchemy async (asyncpg driver, $-style params)
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
class SqlAlchemyQueryable:
def __init__(self, session: AsyncSession): self._s = session
async def query(self, sql, params=()):
# translate $1.. to :p1.. for SQLAlchemy bind params
bound = {f"p{i}": v for i, v in enumerate(params, 1)}
stmt = text(__import__("re").sub(r"\$(\d+)", r":p\1", sql))
return [dict(r._mapping) for r in (await self._s.execute(stmt, bound)).fetchall()]
compact(adapter, scope, RetentionPolicy(keep_versions=N)) prunes old versions
(the only operation that deletes rows; never the latest version or a latest
tombstone).
Inspector (read-only dashboard API)
The second door: structured, read-only JSON for humans and UIs — same adapter, same scope rules, no parser. Build one per request with the scope from your auth, and wrap the four methods as HTTP endpoints:
from virtual_shell import create_inspector, Scope
inspector = create_inspector(adapter, Scope(tenant_id=customer.id, agent_id=agent.id))
await inspector.tree("/") # GET …/fs/tree?prefix= → metadata, never content
await inspector.read(path, version) # GET …/fs/file?path=&version= → inline record or {blob, url}
await inspector.history(path) # GET …/fs/history?path= → all versions, tombstones flagged
await inspector.changes(cursor) # GET …/fs/changes?cursor= → append-only feed (poll every 2–3 s)
read returns an InlineRead (full content) for inline files, or a BlobRead
(metadata + a presigned GET URL) for blob-tier files. changes(None) starts
"from now"; pass the returned cursor on the next poll to get only new events.
The Inspector is strictly read-only — any future human edit must be routed as an
ordinary adapter write (a new version).
Large files (blob tier)
Wrap any adapter in a TieredAdapter to offload files over a threshold (default
100 KB) to a BlobStore, keeping an index (preview, type metadata, search text)
in the row. Bytes go to the blob store; Postgres finds the file, a blob scan
finds the line.
import aioboto3
from virtual_shell import TieredAdapter, S3BlobStore, create_shell, Scope
session = aioboto3.Session()
async with session.client("s3") as s3:
blobs = S3BlobStore(client=s3, bucket="agent-memory")
adapter = TieredAdapter(pg_adapter, blobs) # inline_threshold_bytes=102400
shell = create_shell(adapter, Scope(tenant_id=customer.id, agent_id=agent.id))
S3BlobStore takes an injected client (the AWS SDK stays an optional peer).
Keys are content-addressed (<tenant>/<sha256>), so identical bytes are stored
once and mv copies no bytes. grep is two-stage: a prefix search finds large
files by index; grep "term" /path/to/large/file scans that one file's lines.
Recommended bucket configuration (host's responsibility, not code): server-side encryption (SSE-S3 or SSE-KMS), a private bucket policy, and a lifecycle rule transitioning objects to Infrequent Access at ~90 days and Glacier at ~365 days. Content-addressed keys are immutable, so transitions are safe.
Development
uv sync --extra dev
uv run pytest
uv run mypy
uv run ruff check
License
MIT © Camilo Jiménez
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agent_virtual_shell-0.1.0.tar.gz.
File metadata
- Download URL: agent_virtual_shell-0.1.0.tar.gz
- Upload date:
- Size: 28.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
422f3e691782ea612995068a1bcabc9978d104076c2d76ee28e82fd899332f08
|
|
| MD5 |
a914b0b9a34e5ecda742b0cfa16cbbb3
|
|
| BLAKE2b-256 |
6cde2a6dbb1c6f90a6373530f3bf61254f7ca1212f13d1ab239826aed36b3362
|
Provenance
The following attestation bundles were made for agent_virtual_shell-0.1.0.tar.gz:
Publisher:
release.yml on camilohjimenezdev/virtual-shell
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_virtual_shell-0.1.0.tar.gz -
Subject digest:
422f3e691782ea612995068a1bcabc9978d104076c2d76ee28e82fd899332f08 - Sigstore transparency entry: 1862146449
- Sigstore integration time:
-
Permalink:
camilohjimenezdev/virtual-shell@211ca29c9e3ecdc3bdc8a5a9ef3449c2fc472be9 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/camilohjimenezdev
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@211ca29c9e3ecdc3bdc8a5a9ef3449c2fc472be9 -
Trigger Event:
release
-
Statement type:
File details
Details for the file agent_virtual_shell-0.1.0-py3-none-any.whl.
File metadata
- Download URL: agent_virtual_shell-0.1.0-py3-none-any.whl
- Upload date:
- Size: 37.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a5e992f53a0a48a30c7ae63f63f388356d8042c4d4df64172726665282b86439
|
|
| MD5 |
200cfaf7b82d8d1c909010c1b6775aac
|
|
| BLAKE2b-256 |
1aedb10859383b1bdbd1d22056a399d9a3da4029650f3ab6c89b562af7d312ef
|
Provenance
The following attestation bundles were made for agent_virtual_shell-0.1.0-py3-none-any.whl:
Publisher:
release.yml on camilohjimenezdev/virtual-shell
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
agent_virtual_shell-0.1.0-py3-none-any.whl -
Subject digest:
a5e992f53a0a48a30c7ae63f63f388356d8042c4d4df64172726665282b86439 - Sigstore transparency entry: 1862146521
- Sigstore integration time:
-
Permalink:
camilohjimenezdev/virtual-shell@211ca29c9e3ecdc3bdc8a5a9ef3449c2fc472be9 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/camilohjimenezdev
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@211ca29c9e3ecdc3bdc8a5a9ef3449c2fc472be9 -
Trigger Event:
release
-
Statement type: