Python Environments And Skills
Environment providers give Python agents controlled access to filesystem, shell, resource, and resumable environment state. Skills are native Starweaver skill packages discovered through an environment provider.
Virtual Environments
VirtualEnvironment(...) and EnvironmentProvider.virtual(...) are
deterministic and useful for tests:
from starweaver import VirtualEnvironment
environment = VirtualEnvironment(
files={"README.md": "hello"},
shell_outputs={"pwd": {"stdout": "/workspace\n", "exit_code": 0}},
)
EnvironmentProvider.virtual(...) remains available for code that prefers the
factory style.
The virtual provider can be passed to create_agent(...), agent.session(...),
or one run(...).
Local Environments
LocalEnvironment(...) and EnvironmentProvider.local(...) wrap a Rust local
provider with explicit read/write and shell policy:
from pathlib import Path
from starweaver import LocalEnvironment
environment = LocalEnvironment(
Path.cwd(),
allowed_paths=[Path.cwd()],
context_file_tree_roots=[Path.cwd()],
writable=False,
allow_shell=False,
)
allowed_paths controls tool authority. context_file_tree_roots controls
what may be rendered into prompt context. Prompt visibility does not imply
filesystem authority.
Use render_context() to preview the provider-supplied model-facing
environment context. This is useful for validating that context_file_tree_roots
does not expose auxiliary roots that are available through allowed_paths for
tool execution.
Envd Environments
EnvdEnvironment.from_local(...), EnvdEnvironment.http(...), and
EnvdEnvironment.stdio(...) are semantic constructors over the same native envd
providers exposed by EnvironmentProvider.envd_local(...),
EnvironmentProvider.envd_http(...), and EnvironmentProvider.envd_stdio(...):
from starweaver import EnvdEnvironment, VirtualEnvironment
backing = VirtualEnvironment(files={"README.md": "hello"})
environment = EnvdEnvironment.from_local(backing, environment_id="product-workspace")
The Environment base name is a semantic facade over EnvironmentProvider.
These names do not move policy, state, process ownership, or envd protocol
handling into Python.
Direct Environment Operations
Environment providers expose direct async methods for applications and tests:
from starweaver import Environment
async def inspect_environment(environment: Environment) -> None:
text = await environment.read_text("README.md")
entries = await environment.list_with_options("", max_entries=20)
assert text or entries
For application-facing ergonomics, use environment.files and
environment.shell. These facades still delegate to the same Rust provider
policy:
from starweaver import Environment
async def inspect_with_facades(environment: Environment) -> None:
text = await environment.files.read("README.md")
typed_entries = await environment.files.list_dir_with_types("")
shell_output = await environment.shell.execute("pwd")
assert text or typed_entries or shell_output
Shell.execute(...) covers foreground commands. Shell.start(...),
wait_process(...), write_stdin(...), send_signal(...), and
kill_process(...) expose provider-owned background process snapshots where
the provider supports ProcessShellProvider:
from starweaver import Environment
async def run_background(environment: Environment) -> None:
process = await environment.shell.start("sleep 5")
assert process.running
await environment.shell.send_signal(process, "TERM")
killed = await environment.shell.kill_process(process)
assert killed.terminal
The handle is a ShellProcess snapshot. The Rust provider owns the live
process and returns updated snapshots; Python does not keep a parallel process
store.
Python-Defined Providers
Subclass PythonEnvironmentProvider when product code already owns the
workspace, resource, or remote execution boundary. Convert it with
EnvironmentProvider.from_python(...) before attaching it to agents or
toolsets:
from typing import Any
from starweaver import EnvironmentProvider, PythonEnvironmentProvider
class ProductEnvironment(PythonEnvironmentProvider):
def __init__(self) -> None:
super().__init__(id="product-workspace")
self.files = {"README.md": "hello"}
async def read_text(self, path: str) -> str:
return self.files[path]
async def write_text(self, path: str, content: str) -> None:
self.files[path] = content
async def stat(self, path: str) -> dict[str, Any]:
return {"size": len(self.files[path]), "is_file": True, "is_dir": False}
async def list(self, path: str = "") -> list[str]:
return sorted(self.files)
async def run_shell(self, command: dict[str, Any]) -> dict[str, Any]:
return {"status": 0, "stdout": "", "stderr": "", "metadata": {}}
async def bind_product_environment() -> EnvironmentProvider:
return EnvironmentProvider.from_python(ProductEnvironment())
The native bridge schedules sync or async Python callbacks on the captured
event loop and validates results against the Rust EnvironmentProvider trait.
Core callbacks cover read_text, read_bytes, write_text, create_dir,
delete_path, move_path, copy_path, write_tmp_file, stat, list,
run_shell, and export_state. Background process methods remain available
from native providers that implement the Rust ProcessShellProvider extension.
Use WorkspaceBinding and VirtualMount when an application needs multiple
providers behind one agent-facing environment namespace:
from starweaver import EnvironmentProvider, VirtualMount, WorkspaceBinding
async def bind_workspace() -> None:
workspace = EnvironmentProvider.virtual(id="workspace", files={"README.md": "workspace"})
data = EnvironmentProvider.virtual(id="data", files={"table.csv": "x,y\n1,2\n"})
environment = WorkspaceBinding(
[
VirtualMount("workspace", workspace, default=True, default_for_shell=True),
VirtualMount("data", data, mode="read_only"),
],
id="workspace-binding",
).environment()
assert await environment.read_text("README.md") == "workspace"
assert await environment.read_text("/environment/data/table.csv")
Routing, read-only enforcement, default shell selection, and process-id rebasing
come from Rust CompositeEnvironmentProvider.
Use envd-backed providers when an application needs the provider boundary to go
through the envd service contract. envd_local(...) wraps an existing provider
with an in-process LocalEnvd service; envd_http(...) and envd_stdio(...)
connect to a remote envd endpoint or child process:
from starweaver import EnvironmentProvider
async def use_envd() -> None:
backing = EnvironmentProvider.virtual(files={"README.md": "envd"})
environment = EnvironmentProvider.envd_local(
backing,
environment_id="app-workspace",
id="app-envd",
)
assert await environment.read_text("README.md") == "envd"
The envd adapter still exposes the same file, shell, process, context, and state methods. Environment state includes envd metadata such as the envd environment id, kind, store, state version, and operation/effect ids.
For model-facing access, attach first-party environment toolsets:
from starweaver import create_agent, environment_toolsets
from starweaver.testing import TestModel
agent = create_agent(
model=TestModel.text("ready"),
environment=environment,
toolsets=environment_toolsets(),
)
Environment State
Environment handles are process-local. Durable Starweaver session state can store environment state snapshots, but Python applications must reattach live providers after restore:
from starweaver import SessionArchive
archive = SessionArchive.from_session(session)
restored = agent.session_from_archive(archive, environment=environment)
Use session.export_environment_state() or environment.export_state() when a
host application needs to inspect provider state explicitly.
Skills
SkillRegistry.scan(...) loads Starweaver SKILL.md packages from an
environment provider. It is not a Python-only skill format.
from starweaver import EnvironmentProvider, SkillRegistry, SkillSourceScope
async def load_skills() -> SkillRegistry:
environment = EnvironmentProvider.virtual(
files={
"skills/research/SKILL.md": """---
name: research
description: Research workflow
---
Read primary sources before answering.
"""
}
)
return await SkillRegistry.scan(
environment,
SkillSourceScope(root="", directories=["skills"]),
)
Pass the registry directly to create_agent(...):
agent = create_agent(model=model, environment=environment, skills=skills)
Use skills.toolset() when the model should search and activate skills through
the normal toolset composition path.