Context, configuration, and files¶
This lesson connects three related but separate APIs:
ToolContextis the immutable dependency bundle a runner gives a tool;load_config()explicitly loads ZeoCore configuration;FileSystemServiceperforms sandboxed file operations and returns typed results.
ZeoCore does not load configuration or create tool dependencies merely because you imported it.
ToolContext: dependencies belong to the runner¶
The canonical example constructs context like this:
import logging
from tempfile import TemporaryDirectory
from zeo_core.core.fs import create_service
from zeo_core.tools import ToolContext
with TemporaryDirectory() as tmp:
ctx = ToolContext(
run_id="lesson-001",
tool_name="reader",
tool_version="1.0.0",
logger=logging.getLogger("reader"),
fs=create_service(base_dir=tmp),
work_dir=tmp,
output_dir=tmp,
services={},
metadata={"environment": "tutorial"},
)
The required fields identify one run and provide a logger, filesystem, working
directory, and output directory. services holds optional integrations.
metadata must be JSON-safe. The context and the top level of both mappings
are immutable; tools should treat nested values as read-only too.
Inside a capability, use the public accessors:
logger = ctx.require_logger()
fs = ctx.require_fs()
optional_calendar = ctx.get_service("calendar")
required_calendar = ctx.require_service("calendar")
work_dir = ctx.work_path
output_dir = ctx.output_path
get_service() returns None when absent. require_service() raises
ValueError, so only use it when the runner contract guarantees that service.
A tool should not silently create a missing integration.
Load configuration explicitly¶
Use the public configuration entry point:
from zeo_core.config import load_config
config = load_config()
print(config.general.project_name)
print(config.logging.level)
With no explicit path, load_config() searches its default locations and, if
none exists, uses built-in defaults plus environment values. It does not fail
just because no file exists.
For a specific YAML file:
An explicit path is a promise that the file exists. A missing explicit path
raises ZeoConfigurationError. Run
examples/config_usage.py to see defaults,
a caught missing-path error, and a real YAML file end to end:
Configuration is not a named ToolContext field. The runner decides which
derived values a capability needs and can pass JSON-safe values through
metadata, or provide a dedicated object in services. Avoid having every
tool reach for legacy global configuration state.
Use the filesystem service¶
Import the public service factory, not internal _ops or _internal modules:
Relative paths are anchored to base_dir. Parent traversal is blocked, and
absolute paths outside the base directory are rejected unless the runner
deliberately enables the unsafe trust-boundary option.
Filesystem methods return typed operation results instead of raising for ordinary failures:
write = fs.write_text("output/greeting.txt", "Hello, World!")
if not write.ok:
print(write.error_info)
read = fs.read_text("output/greeting.txt")
if read.ok:
print(read.as_text())
else:
print(read.error_info)
Before writing a nested path, create its directory and check that result:
created = fs.ensure_dir("output")
if not created.ok:
print(created.error_info)
else:
written = fs.write_text("output/result.txt", "done")
print(written.message)
Use the canonical .ok indicator. The filesystem result objects may include a
normalized path, human message, structured error_info, and
operation-specific fields such as content or bytes_written.
Inside a capability, use the injected service:
def run(request: Request, ctx: ToolContext) -> CapabilityResult[Response]:
read = ctx.require_fs().read_text(request.path)
if not read.ok:
return CapabilityResult.fail(
msg=read.message or "Could not read input",
code="ZEO_IO_READ_FAILED",
metadata={"fs_error": read.error_info.model_dump(mode="json")}
if read.error_info
else {},
)
return CapabilityResult.ok(data=Response(text=read.as_text()))
The runner chooses the sandbox root and directories; the capability consumes them. This keeps tests deterministic and host access visible.
For the smallest complete contexts, compare
examples/capability_authoring.py
and examples/minimal_tool.py. Continue with
Results and errors to choose the correct outward
result.