VisExtension Center

Extension API

Reference for Python extension declarations, tool contracts, callbacks and host operations. Start with the tutorial for a complete entry file or Extension design for authoring and test guidance.

Find an API

Task Section
Declare and register an extension Registration
Export a callable or object namespace Tools
Explain when the agent should use it Prompts and discovery
Read types, defaults and introspection limits Tool contracts
Add a user command or guard Slash commands · Op hooks
Persist data or report status Durable state · Logging and notifications
Read environment or add session context Environment · Session context
Access files or start a process Filesystem and processes
Show forms, live work or register a provider Forms · Live views · Providers

Registration

Examples on this page use import blockether.vis.extension as vis inside a trusted extension, not in python_execution. Call vis.register(vis.Extension(...)) once per entry file. The declaration constructors validate and copy values without IO; registration applies them and resolves the declared environment.

Extension arguments are keyword-only. Its collection arguments accept sequences of the corresponding SDK declarations and are stored as tuples, not mutable registries.

Argument Default Meaning
name: str Required Unique extension identity; also keys durable state
description: str Required One-line summary for the extension list and model snapshot
alias: str None; required with symbols Registry identity, not a tool-name prefix
symbols () vis.Symbol declarations for callable tools
prompt: str or callable None Model instructions; a callable receives the env dict each turn and returns text or None
activation None Optional (env) -> bool; False hides the extension for that turn
slash_commands () vis.SlashCommand declarations for user commands
op_hooks () vis.OpHook guards or observers
network_filters () vis.NetworkFilter request/response policy; see network filters
providers () vis.Provider declarations
ctx None Optional (env) -> dict merged into the model's session context
env () Names of host environment variables this entry may read
kind, version None Display metadata; an installed package supplies its manifest metadata

The callback env dict contains cwd, session_id and channel. Keep prompt, activation and ctx short-running. Calls into one extension instance are serialized. Dependency and skill metadata belong to the package manifest, not Extension.

Tools

vis.Symbol(fn_or_object, name=None, tag="observation", is_hidden=False, activity=None)

name=None uses a function's name; set name explicitly for an object namespace. tag is observation or mutation. is_hidden=True removes a callable from model-facing discovery but does not make it inaccessible or authorize its use. Every exported callable needs a nonblank docstring. activity=None uses the default presentation.

Source functions can be ordinary synchronous Python. The model calls their proxies with await in python_execution; arguments and results remain Python values. Exceptions are ordinary tool failures. The execution boundary determines which values can cross to the sandbox.

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 packaged example. 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.

Prompts and discovery

Use prompt for a short explanation of when to choose this extension and the public names to search. For the packaged greeter, a suitable value is:

Use greet.hello to generate greeting text; it never sends messages.
Discover the tools with apropos(r"^greet\.") and read doc("greet.hello") before calling.

Assign this text to Extension.prompt; it is not Python code and does not execute at registration. A prompt callable computes text each turn; it should not log in, start background work or repeat the entire API. description supplies the extension summary even when prompt is omitted.

apropos(pattern) filters public symbol names by regular expression. doc(name) or doc(hit) reads the complete matching document. Put parameter details in the tool's docstring and annotations, not the prompt. Put an optional multi-step procedure in a skill. Neither prompt text nor reading a skill enforces permissions; use explicit policy mechanisms for guards.

Tool contracts

Symbol.contract returns fresh portable data without registration or a tool call. In the sandbox, each callable exposes its contract as an attribute, for example greet.hello.contract. Changing that local dictionary does not change the declared tool or its permissions.

Contract Fields
Callable version, name, tag, description, signature, parameters, returns
Namespace version, name, members with full public member names
Parameter Name, parameter kind, required, has_default, default_is_none, and type description

doc() renders this same contract as the signature, prose, argument types and result fields. This is a documentation contract, not JSON invocation or runtime validation. Python binds arguments; the implementation validates domain constraints. There is no manual schema/signature override. The exact portable shape is the symbol schema.

Defaults and introspection

Non-None runtime default values are withheld, and their repr() is never called. This avoids exposing private host objects or credentials, including values whose Python type looks ordinary. Public default behavior still belongs in the tool's documentation; see documenting defaults.

Python declaration has_default default_is_none Rendered default
Required argument False False No default
Argument with = None True True None
Argument with any other default, including False or 30 True False ...

*args and **kwargs are not required even though they have no default. The original defaults apply when arguments are omitted. ... is a display marker, not an instruction to pass Ellipsis. Dataclass-field defaults and factories are also described without exporting values or running factories.

Sandbox inspection Supported result
tool.contract Portable parameter/result types, fields and documented meaning
doc("tool") Human-readable rendering of that contract
inspect.signature(tool) Names and parameter kinds; None or Ellipsis defaults; no type annotations
tool.__annotations__, typing.get_type_hints(tool) Empty dictionaries, not a supported type-discovery API
tool.__signature__ Not supplied

The original host classes and their identity do not cross the sandbox boundary. Use .contract for type discovery instead of trying to reconstruct host annotations.

Supported types and unresolved annotations

Descriptions cover every parameter kind, return types, dataclass fields, unions, common containers, Literal and string metadata in Annotated. Recursive records use references rather than expanding forever.

  • tuple[T, ...] has variadic: true and one type in arguments; a fixed-length tuple retains each item type and omits variadic.
  • Name (unresolved) means the annotation could not be resolved safely. Vis does not guess, import or evaluate an expression to discover a type.
  • Name (opaque) means the class is known but its structure is not described. Both labels appear in nested types and record fields, not just top-level returns.

Use from __future__ import annotations and module-level result classes. Python 3.14's deferred annotation functions can execute code even when asked for strings; without that import they are reported as unresolved instead of evaluated. Local forward references absent from the defining module remain unresolved.

Cross-module decorators

functools.wraps chains resolve annotations in the wrapped function's defining module, including bound namespace methods and qualified names such as models.Result. Cyclic __wrapped__ chains raise ValueError. These two complete modules illustrate cross-module wrapping and a variadic tuple result:

# decorators.py
from functools import wraps


def traced(fn):
    @wraps(fn)
    def call(*args, **kwargs):
        return fn(*args, **kwargs)
    return call
# tools.py
from __future__ import annotations
from dataclasses import dataclass
from decorators import traced


@dataclass(frozen=True)
class Result:
    text: str


class Tools:
    @traced
    def read(self, text: str = "ready") -> tuple[Result, ...]:
        """Read one result without changing state."""
        return (Result(text),)

vis.Symbol(Tools(), name="tools").contract expands Result.text beneath the variadic tuple. Omitting text uses the public string "ready". The SDK tests execute these snippets, and host-to-sandbox tests cover invocation, nested records, redacted defaults, introspection and refreshed metadata after reload.

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; a session gets its own trusted extension worker on its first extension call. Session disposal stops that session's worker, not the gateway-wide registration worker. Interpreter memory and host-call identities are not shared across workers.

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. Ordinary open() uses the extension's permissions too, not the model's jail.

See also