Extension API
Reference for extension declarations, tool contracts, commands, hooks and host operations.
For a runnable package, start with the quickstart. Dependency and
skill metadata belong to the package manifest, not Extension.
The declaration
Call vis.register(vis.Extension(...)) once per extension file. The
Extension, Symbol, SlashCommand, OpHook, NetworkFilter and Provider
constructors create immutable declarations. Construction validates values but
performs no IO; registration applies them.
| Argument | Type | Purpose |
|---|---|---|
name |
str, required | Unique extension name. |
description |
str, required | One line for vis-agent extension list and the model's extension snapshot. |
alias |
str | Registry identity; required with symbols. It does not prefix tool names. |
symbols |
list of vis.Symbol |
Tools the model can call. See Tools. |
prompt |
str or callable | Text added to the model's prompt. A callable receives the env dict every turn and returns a string or None. |
activation |
callable | (env) -> bool, evaluated per turn. False hides the whole extension for that turn. |
slash_commands |
list of vis.SlashCommand |
Commands for the user. See Slash commands. |
op_hooks |
list of vis.OpHook |
Guards and observers over tool calls. See Op hooks. |
network_filters |
list of vis.NetworkFilter |
Request and response policy at the gateway proxy. See Process jail and network policy. |
providers |
list of vis.Provider |
LLM providers. See Provider extensions. |
ctx |
callable | (env) -> dict, merged into the model's session dict every turn. See Session context. |
env |
list of str | Host environment variables this file may read. See Environment. |
kind, version |
str | Display metadata. |
The env dict passed to prompt, activation and ctx contains only cwd,
session_id and channel.
Keep prompt short. The model finds tools with apropos(pattern) and reads
their docstrings with doc(name). Do not repeat signatures in the prompt. Use
it for additional context, such as a project-specific catalog.
Tools
vis.Symbol(fn_or_object, name=None, tag="observation", is_hidden=False, activity=None)
name is the public sandbox name, defaulting to the function name. tag is
observation or mutation; is_hidden=True leaves the tool callable but removes
it from model-facing discovery. A callable must have a nonblank docstring.
Symbol.contract returns fresh portable data without registration. A callable
contract has version, name, tag, description, signature, parameters and
returns. A namespace contract has version, name and members, each with its
full public name. The engine exposes each callable's description as its .contract
attribute; changing that local dict does not change the declaration or permissions.
Read type descriptions and limitations.
doc(name) uses the same contract for its signature, prose, argument types and
result fields. apropos(pattern) remains a compact name-filtered catalog, not a
schema dump. Operations continue to accept Python arguments and return Python
values; exceptions remain ordinary tool failures.
Return typed objects
A dict is sufficient for simple results. Use frozen dataclasses with annotated
public fields when shape and meaning matter. The tested example
shows field descriptions with Annotated. Private fields and original methods do
not cross into the sandbox.
Object namespaces
vis.Symbol(Greeter(), name="greet") exports greet.hello(...) in the
quickstart. Public methods become tools; object attributes become
nested namespaces. Names beginning with _ are excluded. Public scalars, modules,
classes, cycles and repeated object references are rejected with their path.
vis.method(tag="observation", is_hidden=False, activity=None) overrides metadata
for one method. Metadata never changes the method's Python signature.
Activity presentation
activity=vis.Activity(presenter="tests", label="Run checks") describes how a
running tool is shown in the TUI and the Companion app. Add render=callback
to customize the display. The callback receives phase (start, success
or failure), args, kwargs, result and error and returns a
vis.ActivityPresentation(headline, summary, blocks) or None. Blocks are
heading, text, markdown, code, diff, table, progress, image,
video, audio and file. vis.publish_activity(presentation) replaces the
presentation while the tool runs. Presentation errors never change a tool's
result. Supported block types are defined in the
Activity contract.
Slash commands
vis.SlashCommand(name, run, doc=None, usage=None)
run(ctx) receives {"channel", "args", "raw", "session_id"} and returns
vis.ok(title, body=None, data=None), vis.err(title, body=None, data=None)
or a plain string, which counts as an ok title. body is Markdown.
Op hooks
vis.OpHook(ops, fn, phase="before")
ops names sandbox tools such as "patch", "shell" or "python_execution".
With phase="before", fn(call) receives {"op", "args"} and returns
vis.block(reason) to refuse the call or None to allow it; the model sees the
reason as a tool failure. With phase="after", fn receives {"op", "args", "result"} and its return value is ignored. An error inside a tool hook allows
the call.
ops also names the draft lifecycle: "draft/create", "draft/approve" and
"draft/discard" run for the sandbox's draft_create(), draft_approve() and
draft_discard(). Their args carry the draft's workspace_id, label,
root, repo_root, backend and, for approval, branch, target_branch,
files and message. Approval commits and merges into the default branch;
each new commit also crosses git/commit. A before hook that returns
vis.block(reason) refuses the operation and the user sees the reason. See
Drafts.
"fs_access" checks paths used by the host file tools (cat, grep, patch,
ls). It is not a tool itself and takes no phase. Its callback receives
{"operation": "file-read" | "file-write", "path": <absolute path>}. An error
in the callback refuses the operation. This check does not apply to open()
in the sandbox, which uses the sandbox's filesystem policy.
vis.strings_of(value) collects strings from a nested structure, for example
to check paths in tool arguments.
Durable state
vis.state is a dict-like store persisted in the Vis database. It survives
/reload and restarts and is keyed by extension name, so a project override
shares state with the global extension it replaces and two different extensions
never share.
vis.state["repo"] = "acme/widgets"
vis.state.get("count", 0)
"repo" in vis.state
del vis.state["repo"]
vis.state.update({"repo": "acme/widgets", "count": 0})
It is a collections.abc.MutableMapping, so pop, setdefault, clear,
keys, items, len and iteration behave as on a dict. Values must be plain
data: dicts, lists, strings, numbers and booleans. Writing None removes the
key.
Logging and notifications
vis.log("info", "loaded 3 rules") # trace, debug, info, warn, error
vis.notify("Rules reloaded", "success") # info, success, warn, error
vis.log writes to the gateway log under ~/.vis/logs/. vis.notify shows a
toast in the active channel.
Asking the human and showing live work
vis.ask(title, fields) pauses the extension and shows a typed form in the TUI
or the Companion app. vis.live(title, nodes) opens a view that the extension
updates while a job runs. Both are documented on their own pages:
Asking the human and Live views.
Environment
An extension does not automatically receive the full host environment.
Declare the variables it needs in env. vis.register() resolves them and
adds them to the extension's os.environ; read them after registration.
import os
vis.register(vis.Extension(
name="acme",
description="Acme integration.",
env=["ACME_API_KEY"],
))
key = os.environ.get("ACME_API_KEY") # absent when nothing resolves it
Each name resolves through the project's environment: block, then .env and
.env.local, then the environment that started Vis (see
Configuration). Names defined by the project
itself need no declaration. env= affects only the extension's os.environ,
not the environment of jailed child processes. To pass a variable to a jailed
child, declare it under environment:.
Session context
A ctx callable adds data to the model's session dict each turn:
def _ctx(env):
return {"session_env": {"todo": {"open": len(vis.state.get("todos", []))}}}
vis.register(vis.Extension(name="todo", description="Todo list.", ctx=_ctx))
Return a string-keyed dict under a key unique to your extension. Results from all extensions are deep-merged. A non-dict return or exception adds no context and does not block the turn.
Filesystem and processes
Extension code runs in a trusted process, separate from the model's sandbox. A gateway-wide worker loads registrations. Each session gets a separate trusted extension worker on its first extension call; session disposal stops both workers. The workers do not share interpreter memory or host-call identities.
| Model sandbox | Extension context | |
|---|---|---|
| Author | the model | you |
| Filesystem | workspace roots when the jail is enabled | your user's permissions |
| Network and processes | gateway policy; no direct spawn | unrestricted |
| Environment | project values | declared env plus project values |
Native calls through ctypes |
refused under confinement | supported |
| Lifetime | session worker | separate registration or session worker; reloaded by /reload |
Tool results cross as data. Objects and dataclasses expose their public fields as frozen data records in the sandbox, including nested records. The original class, methods, native pointers and object identity stay in the trusted process. Expose operations as declared tools rather than methods on returned objects.
subprocess, os.system and vis.shell({...}) run without the jail. Output
not read by the extension is captured in its log. Child processes receive
pipes rather than a terminal; output is drained into an 8 MiB buffer per
stream, including while the extension waits for the child to exit.
To confine a child, use a jailed shell:
| Call | Policy | Needs a session |
|---|---|---|
vis.shell({...}) |
none | no |
vis.jailed_shell({...}) |
merged configuration on disk, read at each spawn | no |
vis.jailed_shell_session({...}) |
the invoking session's policy snapshot | yes |
vis.fs provides filesystem operations with extension permissions: mkdir,
write, read (bytes), read_text, copy, move, list, stat and remove.
Use ordinary open() for paths inside the session's roots.
Calls into one extension instance are serialized. Keep prompt, activation
and ctx short-running; tools can perform longer operations.
See also
- Extension packages — installation and reload.
- Extension troubleshooting — loading and call errors.
