Installing and sharing extensions
You can keep an extension in one project, install it for all your projects or share it with other people. This guide covers those choices and how to reload changes.
When to use
- An extension needs third-party dependencies or more than one file. Choose a layout and describe the package in a manifest.
- You want an extension in every project, not only one. Install it for all your projects. Where extensions load explains how project and global copies combine.
- Everyone on your team should get the same extensions. Save the installation in the project's configuration.
- You want to install an extension someone else wrote. Prefer an approved release. Otherwise, pin the commit that you reviewed. Install source that is not in the catalog shows how.
- An update broke something. Roll back to the version that worked.
- You want to share your extension. Publish it through GitHub Releases.
- You do not want your installs to count as downloads. Download counts explains what Vis reports and how to turn it off.
- Settings show a switch for a skill that your extension always needs. Leave the skill out of settings.
- Your workspace holds several repositories, each with its own extensions. Load extensions from nested projects.
For your first tool, start with Extending Vis. If you already have a uv package, follow Using an existing Python project.
Choose a layout
| Situation | Layout | Dependency preparation |
|---|---|---|
| Small local tool using the standard library | .vis/extensions/greeting_tools.py |
None (Vis supplies the SDK) |
| Local script with third-party wheels or source roots | PEP 723 entry file | Automatic wheel installation at load/reload |
| Package to install or share, optionally with skills | pyproject.toml and extension.py |
Automatic uv preparation at load/reload |
| Existing uv project with an editable implementation | Entry file declaring tool.vis.project |
Automatic preparation on project admission or reload. See existing Python projects |
Choose one dependency mode per entrypoint. A package does not need a skill, a provider does not need tools, and a small tool does not need a package.
Where extensions load
| Directory | Scope |
|---|---|
~/.vis/extensions/ |
Every project |
<project>/.vis/extensions/ |
That project |
A project extension with the same registered name overrides the global extension.
Top-level .py files are entries. Installed packages live at
.vis/extensions/<name>/<version>/, with source files directly inside the version
directory. A current link inside <name>/ selects the active version. Vis loads
only its extension.py, never every installed version. Test files (test_*.py and
*_test.py) are not extension entries.
For example, installing vis-greeter version 1.0.0 creates:
.vis/extensions/
vis-greeter/
1.0.0/
extension.py
pyproject.toml
src/
current -> 1.0.0
GitHub versions contain copied source and an installation receipt. Local versions
link to your development checkout instead, so edits stay in that checkout. The
installer adds Git ignore rules for installed package directories and its private
metadata. Loose .py extensions remain visible to Git. Commit package declarations
in vis.yml, not downloaded sources or installation receipts.
Review extensions before adding or opening a project. Their entry files and
dependencies run with your user permissions, outside the model's jail. Adding a
project or opening its session authorizes Vis to install and load its configured
extensions. Review vis.yml and local overrides before opening an unfamiliar
checkout. CLI installation and sync commands require --trust. Validation is not
a security review.
Declare packages in configuration
Use the same extensions map in project vis.yml and global ~/.vis/config.yml
(or ~/.vis/vis.yml). Keys are normalized package names from pyproject.toml:
extensions:
vis-tools:
source: https://github.com/example/vis-tools
subdirectory: extensions/vis-tools
version: "0.1.0"
vis-notes:
source: https://github.com/example/vis-notes
version: latest
vis-greeter:
source: ./tools/greeter
Replace the example repository and version with a reviewed, approved release. A declaration accepts
source, optional subdirectory, and either version or a full lowercase Git revision. A
version is an approved release such as "0.1.0", or latest to track the newest approved stable
release.
The source can also carry the folder (example/vis-tools/extensions/vis-tools). Name the folder
only once. Selectors apply only to GitHub sources. Local paths are relative to the YAML file that
declares them, not to the shell's working directory.
When you add a folder in Projects, or open a session in that project, the gateway does these steps:
- It reads the merged configuration of the project.
- It installs missing declared packages.
- It prepares their Python dependencies.
- It makes the tools available.
Preparation can need network access and can take longer on the first open. If installation or loading fails, Vis reports the failure. It does not open a session with incomplete tools. Fix the declaration or dependency problem and try again.
One gateway can serve several projects at once. Each session resolves tools,
providers, environment values, and toggles from its own project, not the directory
where the gateway started. Project extensions override same-name global extensions
only in that project. Opening another project does not replace those tools.
Package and declared uv project environments are separate per extension. PEP 723
wheels and shared sync installs go to ~/.vis/python/packages, which every project
on the gateway shares.
Use /reload in a project session to adopt later configuration or source changes.
It reloads that project and shared global extensions, not other projects' local
catalogs. Reopening an already admitted project reuses its loaded code.
To preview or prepare packages without opening a session, use the CLI:
vis-agent extension sync --dry-run
vis-agent extension sync --trust
vis-agent extension sync --project --trust
vis-agent extension sync --global --trust
vis-agent extension sync --refresh --trust
By default, sync manages both scopes. Global declarations go to ~/.vis/extensions/, and project
declarations go to <project>/.vis/extensions/. Local project overrides in .vis/config.yml
replace complete declarations by name, not individual fields. When both scopes use the same
extension name, the project registration wins.
Reading configuration alone never installs packages. Preparation happens when you add or open a
project, and when you reload explicitly. Standalone CLI sync requires --trust.
Sync installs missing packages, reconciles changed declarations and prepares their
uv environments without importing entrypoints or reloading a running gateway.
It reports installed, updated, cached, orphaned or failed per package.
A failure exits nonzero. Start Vis or use /reload to activate prepared code.
Without a selector, the first sync pins the latest approved stable release. An unchanged declaration
reuses that SHA without fetching Git or the catalog. Only --refresh checks for a newer approved
release. Explicit versions stay fixed, also when you request a refresh. Changing a version can
downgrade on purpose.
To track releases instead of pinning, use version: latest. Then every sync checks the catalog.
When the newest approved stable release changes, sync installs it without --refresh. It never
selects a prerelease. sync --dry-run reports it as would-sync, because a dry run makes no
network calls.
Dependencies use uv sync --check --offline first. They fall back to normal uv sync only when
preparation is needed. uv owns its lockfile, environment and download cache. A ready environment
is recorded in its .venv, so the next start skips uv until the project, lock or packages change.
When you remove a declaration, sync keeps its installation as orphaned. Preview removals with
sync --dry-run --prune. Then use sync --trust --prune to unlink them. Sync can remove only
current links that it still owns. It keeps development checkouts and installed version directories.
Sync never adopts or replaces links that you installed manually or that changed outside sync. Keep
them and resolve the conflict before you sync. Do not edit the private .sync.json receipt. Do not
run a manual update or rollback on sync-owned packages. Change their declarations instead. A dry run
writes nothing and does not fetch sources, install dependencies or run extension code.
When you copy or move a project, sync reuses the unchanged GitHub versions in its .vis/extensions
directory. If sync refuses a package, the error names the package directory. Keep any local edits
from that directory, remove it and run sync again.
Load extensions from nested projects
Your workspace can hold several repositories, and each one can keep its own extensions in
<repository>/.vis/extensions/. Vis does not search those directories by itself. Declare them in
the workspace vis.yml with a glob source:
extensions:
repositories:
source: ./repositories/*/.vis/extensions/*.py
local-packages:
source: ./packages/*
Vis expands each glob when it loads extensions and again at each /reload. The key, such as
repositories, only names the declaration in error reports. The glob gives these results:
- A matched
.pyfile loads as a single-file extension. Test files are not entries. - A matched directory loads its
extension.py, or itscurrent/extension.py.
A glob is relative to the YAML file that declares it. Each *, ? or [...] matches inside one
directory level. Vis refuses **. A pattern matches a hidden name only when its own level starts
with ., as .vis does above. A glob declaration accepts only source. Vis loads matches in
place and never installs them, so extension sync ignores them.
Each match loads on its own. A file that fails to load, or a glob that Vis refuses, appears in
the load report and in vis-agent doctor. The other matches still load. Each extension needs its
own registered name. If a matched file uses the name of an extension that is already loaded, the
matched file fails with an error that names both files.
Each extension also gets its own project root: the nearest directory above its file that holds
a .vis directory. In the example, that is repositories/<name>. Extension code reads it with
vis.project_root(), so the same file works in the workspace and
in a session that opens the repository directly.
The rules of a nested project also apply to its extensions. When such an extension is active, the
session's project rules list the AGENTS.md or CLAUDE.md file of its project. The model reads that
file before it uses the project's tools or works in the project. Vis does not copy the file into each
request, so many nested projects do not make the prompt larger.
Install a package
Prerequisites: an installed Vis, and source and dependencies that you reviewed. Vis supplies
uv. GitHub installs also need Git on PATH.
By default, install installs a package directly without changing configuration.
Add --save to also record it under extensions:. --project and --global
choose the scope. --save chooses whether to persist the declaration.
-
In the target project, link the local greeter example after copying its complete directory to
greeter/:vis-agent extension install ./greeter \ --project \ --trustThe local checkout is linked, not copied. You may also pass its
pyproject.toml. Prefix a relative checkout path with./when it could look likeowner/repository. A bare repository slug always selects GitHub, even if a matching local directory exists. Choose--projectfor this project or--globalfor every project. Without either flag, installation is global, even when your current directory containsvis.yml. -
Start Vis there, or run
/reload. Vis prepares dependencies before registration. -
On the next turn, ask Vis to inspect
doc("greet.hello")and callawait greet.hello("Ada"). The result's.textisHello, Ada!.vis-agent extension listchecks registration, not execution.
Save an installation in configuration
Use --project --save to install a package and record it in your project's vis.yml
(or the existing vis.yaml):
vis-agent extension install example/greeting \
--version 1.2.0 \
--project \
--save \
--trust
The key is the package name from pyproject.toml. Approved GitHub releases save an exact version.
--revision saves the reviewed commit instead. Local sources save a path relative to the project
configuration.
With --global --save, Vis writes to its global machine store, ~/.vis/state.yml. It does not
rewrite the global configuration that you wrote by hand. Without a scope flag, the scope is still
global.
Saved packages are managed by extension sync.
Commit the project declaration to share it with your team. To update a saved package,
edit its declaration and run vis-agent extension sync --project --trust. Do not
use manual update or rollback on a saved package.
Saving keeps your comments and other settings. Vis never overwrites a different declaration
with the same name. If saving fails, Vis also removes the new installation. To record a package
that you already installed, run the same install command again with --save.
Install an approved GitHub Release
The Extension Center lists approved releases of public GitHub projects. Publishing an extension on PyPI is not required. Review the selected version's source, manifest and dependencies before installing. Choose where you want to use it:
Project — run this from the target project:
vis-agent extension install example/greeting \
--version 1.2.0 \
--project \
--trust
Global — make the extension available in every project:
vis-agent extension install example/greeting \
--version 1.2.0 \
--global \
--trust
For an extension inside a monorepo, add its project folder to the identifier:
vis-agent extension install example/extensions/tools/greeting \
--version 1.2.0 \
--project \
--trust
Replace the example repository and version with an approved listing. In the Extension
Center, choose Version and the Global or Project scope, then copy the
install command. Older approved releases remain selectable and linkable. Use
vis-agent extension versions example/greeting to list them in your terminal.
Without --version, a GitHub install selects the latest approved stable version,
never the default branch. Prereleases require explicit --version. Unknown, pending
or rejected versions fail without falling back to another version, tag or branch.
The catalog resolves the version to its approved full SHA. Git fetches that commit
and the installer checks the manifest identity and runtime requirements again.
Moving or deleting a GitHub tag cannot change an approved version's SHA.
Use one of these to name the extension:
- The catalog identifier shown on the extension page:
owner/repositoryorowner/repository/folder. - The
github.com/owner/repositoryHTTPS URL of the extension. - The extension page URL itself.
Do not use a file or tree URL. Source that is not listed installs from a commit you pin yourself.
GitHub installation stages and validates only the selected project before atomically activating a source snapshot. The catalog stores no source distributions. Submodules and Git LFS are not fetched. Symlinks are refused. Keep required source and portable dependency paths inside the selected project, within 4096 entries and 64 MiB.
Install source that is not in the catalog
A listing is not required. When an extension, or the version you need, is not in the
catalog, pin the exact commit you reviewed with --revision instead of --version:
vis-agent extension install example/greeting \
--revision 8f4c1d2e5a9b70c3e61d84af2b5079cc31de6a04 \
--subdirectory extensions/greeting \
--project \
--trust
Copy the SHA from the commit on GitHub, or read it with git ls-remote. It must be the
full, lowercase 40-character commit SHA. Branch and tag names are refused because they can
move. --subdirectory names the package folder in a repository, and --save records the
commit in your configuration for extension sync.
Vis fetches that commit, verifies it is the one you named and applies the same manifest and
runtime checks as a listed release. Only catalog moderation is skipped, so review the source
and its dependencies before you pass --trust.
versions, update and rollback work on approved releases and report none for a pinned
commit. Move to newer code by installing its SHA, or by editing revision: in your
declaration and running vis-agent extension sync --trust. The source must carry a new
project.version: an installed name and version cannot be rebound to another commit.
When the code is your own, a local checkout is simpler than a pinned commit.
vis-agent extension install ./greeting --project --trust links the directory. After /reload,
your edits are live.
Check for updates and roll back
Use the GitHub owner/repository slug, such as example/greeting, not the Python
package name (vis-greeter). Repository names are case-insensitive. Project folders
keep their case. A full HTTPS GitHub repository URL also works.
vis-agent extension versions example/greeting --project
vis-agent extension update example/greeting --project --trust
vis-agent extension update example/greeting --version 1.3.0 --project --trust
vis-agent extension rollback example/greeting --project --trust
vis-agent extension rollback example/greeting --version 1.1.0 --project --trust
Vis finds the installed repository in the selected global or project scope. If you
installed several extensions from that repository, name the folder in the identifier to
select one. Vis refuses to guess. Use --subdirectory . to select its root project.
Before installation, versions needs the folder shown in the catalog, for example:
vis-agent extension versions example/extensions/tools/greeting
versions shows the installed version, approved history, latest stable version and whether an
update is available. update selects a newer approved stable release. --version explicitly
selects a newer release, including a prerelease. It never downgrades implicitly.
rollback restores the previous installed version, or an older approved version that you select
with --version. Installed versions are reused, including any local edits. A version that is not
installed is fetched and validated. A package name and version cannot be reused for a different Git
commit.
An explicit update or rollback keeps the package's synchronization record consistent with its active version. If configuration pins a version or revision, change that declaration to keep a different version after synchronization.
All source changes are explicit and require --trust. A failed fetch, compatibility
check or activation leaves the active installation unchanged. After success, start
Vis or use /reload to prepare dependencies and activate the code in running sessions.
Source rollback does not undo extension state, external side effects or dependency
changes already made by a build backend. Review release notes before changing versions.
The Python manifest name and normalized version determine the installation path.
The name is not the GitHub command identifier. There is no separate version store
or nested project directory. Updating changes the current link atomically and
keeps previous version directories available for rollback.
Do not edit receipts or delete versions you still need. Local development links and
unmanaged directories are never replaced by update or rollback. Use their source
workflow instead. Old installation layouts are not loaded or migrated. Preserve any
local edits before removing an old installation and installing it again.
Download counts
The Extension Center shows how many times each extension was downloaded. Vis reports one download after it fetches an approved release to install, update, roll back or sync an extension. The report contains only the catalog identifier of the extension. A failed report does not affect the installation.
The Extension Center counts each network address one time for each extension in one UTC day. It does not store the address. It keeps a private code for that day only, to prevent repeated counts. Several computers on one network count as one download on that day. Vis does not report a version that is already on your computer, a pinned commit or a local folder.
To stop these reports, set DO_NOT_TRACK=1 in your environment:
DO_NOT_TRACK=1 vis-agent extension install example/greeting --project --trust
Publish a package
Publish and maintain releases
There is no vis-agent extension publish command. Publication uses GitHub Releases
and catalog moderation. Local installation and activation are separate steps.
Extension Center displays each extension as lowercase owner/repository, using
GitHub's repository owner rather than a package author or submitter-provided name.
The project folder distinguishes packages in a monorepo. Use this repository identity
for install, versions, update and rollback. Keep project.name as the Python
distribution name for dependency metadata, internal storage and package-prefixed release tags.
- Commit
pyproject.toml,extension.py, required source and optional skills together in a public GitHub repository. Use a staticproject.version, and commituv.lockfor reproducible dependencies. Keep credentials and private deployment details out of it. - Test the package with the supported Vis and Python versions, including a registered tool call. Bump the manifest version for each new release. Update the lockfile when needed.
- Tag that commit and publish a GitHub Release, not only a Git tag or a draft.
Use
v1.2.0, orPACKAGE-NAME/v1.2.0for an independently versioned monorepo package. For example, anextensions/vis-greetingpackage can usevis-greeting/v1.2.0. The version suffix must exactly matchproject.version. - Submit the repository and project folder to Extension Center once. The optional Release tag field selects a monorepo release or prerelease. Otherwise review uses GitHub's latest stable release. Review the pinned metadata and submit for moderation.
- After approval, publish later GitHub Releases in the same repository and folder. Scheduled discovery validates them and queues new versions for moderation. You do not submit another form. The current approved version stays available during review.
- After approval, run
vis-agent extension versionswith the identifier from the extension page. Confirm the available version and its reviewed commit SHA.
Catalog releases use canonical MAJOR.MINOR.PATCH, optionally followed by aN, bN
or rcN, for example 1.3.0rc1. Mark prereleases on GitHub too. Local package validation
still uses the SDK's PEP 440 rules. GitHub Release notes are the place for changelogs,
breaking changes and supported runtime versions. No release asset, wheel, PyPI account
or publisher credential in Vis is required. Declared dependencies may still use PyPI.
Discovery runs every five minutes. Each run checks one registered listing and one page of up to 20 releases. It inspects at most five new releases per run. Its cursor resumes within a page and rotates across listings and older pages. So detection is not immediate, and it depends on catalog size and GitHub availability.
Only approved versions are public. Vis keeps each rejection, so the same release does not go into the queue again. A version's commit cannot be replaced. To fix a rejected or moved-tag release, publish a new version. Never retag an approved version.
Listing checks metadata and required files. It is not a code audit or an endorsement.
Vis catalog maintainers can use the
Publish reviewed extension workflow
after reviewing the source and dependencies. It requires access to the Vis repository
and its docs environment. It is not an automatic approval path for publishers.
The operator guide
documents its inputs, authenticated moderation and verification.
Publishing or approving a release does not install it or update installed copies. Install or update
a managed package explicitly, or use a configuration
declaration. Project admission and /reload reconcile
declarations. Unchanged pinned declarations keep their installed revision, but version: latest can
fetch a newer approved release. For an existing managed installation, use the update and rollback
commands. Local source links follow their development workflow
instead.
Reload, update or remove
| Change | Action |
|---|---|
| Edit an entry, helper module, declared source root or bundled skill | /reload, then call the tool on the next turn |
| Change dependencies in an existing package environment | Deliberately update uv.lock if needed, then /reload |
| Change dependencies used from shared packages | Run shared sync, then /reload |
| Change an editable project's dependencies | Update its dependency declaration and lockfile, then /reload |
| Change a managed GitHub version | Explicit extension update or extension rollback, then /reload |
| Uninstall | Remove the package’s current link, then /reload. Keep version directories or remove them separately. Do not delete a linked development checkout |
Install never overwrites an existing destination. /reload reconciles configured
packages and rebuilds extension contexts from the selected source. Unchanged pins
do not fetch a newer revision. Already running calls may finish with old code. Live
sessions switch at the next turn boundary.
If a package cannot prepare or load, Vis reports its name, location and error. Other extensions and the core tools remain available.
You can open the project and use Vis to repair the files or configuration. Run vis-agent doctor to inspect the failures.
After the repair, run /reload to retry preparation and loading. A successful retry clears the warning without restarting Vis.
A failed /reload shows the affected files and causes in chat. The report stays in the transcript and enters the next model request.
The agent can use that report to repair the extension. Current failures also remain in its context until recovery.
vis.state survives reload and restarts. A failed reload keeps the last working code, contracts,
docs and package skills. It marks them stale with the failure reason and the loaded and requested
source fingerprints. A successful retry clears the warning. See
troubleshooting.
Vis runs admitted copies of entries and declared source roots. Writes beside those
files affect a private copy, not durable project data. Use vis.state for persistence.
Editable projects instead import their live checkout. Reload does not replace the
running gateway binary or its startup environment, and is not a native-library reload.
Package manifest
A distributable package keeps pyproject.toml and extension.py together, at
repository root or in a selected subdirectory:
greeter/
pyproject.toml
uv.lock
extension.py
src/vis_greeter/__init__.py
tests/test_greeter.py
skills/greeting/SKILL.md
skills/greeting/references/style.md
The tested example
uses this manifest. Generate and commit uv.lock before publishing:
[project]
name = "vis-greeter"
version = "1.0.0"
description = "Typed greeting tools and an optional greeting procedure."
keywords = ["greetings", "example"]
requires-python = ">=3.12"
dependencies = ["vis-agent>=0.1.45"]
[tool.vis]
category = "tools"
source_paths = ["src"]
skills = ["skills/greeting"]
[tool.pytest.ini_options]
pythonpath = ["src"]
| Field | Requirement or behavior |
|---|---|
project.name |
The normalized package name must equal the registered extension name |
project.description, project.version |
Supply displayed package metadata |
project.keywords |
Optional catalog tags. Supply at most two unique tags, each 1–24 lowercase letters or digits, with single hyphens between words |
project.requires-python |
Must allow Vis's embedded interpreter. Vis does not download another Python |
project.dependencies |
Must include an unconditional vis-agent requirement compatible with the running release |
tool.vis.category |
Required package metadata: providers, tools or workflows. Catalog filters use tags instead |
tool.vis.source_paths |
Import roots inside the package, such as src, not the vis_greeter package directory |
tool.vis.skills |
Optional relative skill directories, or tables with path and settings. Omit it when no procedure is needed |
Choose tags that describe what your extension helps people do, such as browser or testing.
Tags appear on catalog cards and extension pages.
In the catalog sidebar, select a tag to filter extensions. Select Official to show extensions maintained by the Vis team. You can combine these filters with search. The counters show matching extensions.
You can use your own tags. Tags do not grant the “Official” badge. Publish a new release to change an approved extension's tags.
Keep the implementation under the selected package directory. Do not put a PEP 723
block in this package's extension.py. See Extension design
for the complete registration and implementation.
Vis prepares the package's uv environment before it registers the package. It does this at these times:
- At startup, for global packages.
- When you add or open a project, for the packages of that project.
- On
/reload.
Vis reports a preparation failure. The package does not fall back to ~/.vis/python/packages.
Source-only packages use shared packages when uv selects no dependency beyond the bundled Vis SDK.
Vis imports their declared source_paths without creating .venv.
Projects that need an editable installation keep a private environment.
To prepare a private environment, Vis runs bundled upstream uv sync.
It selects the gateway's embedded Python with --python and blocks interpreter downloads.
uv manages the project's lock and environment, including default dependency groups and removal of extra packages.
It can update an existing lock.
Vis's python.index_url supplies uv's default index,
unless the index environment of uv is already set. To override it, use uv's --default-index. uv
still manages named indexes and package source configuration.
A project that needs its own environment uses a separate trusted worker. It imports dependencies
from that environment, normally .venv, without shared-package fallback. These
project dependencies are not installed into shared sandbox packages. Source-only edits need
reload. Build backends and executable .pth files are trusted code. Imports in
python_execution never install packages.
Bundled skills
Declare up to 64 relative directories containing SKILL.md in tool.vis.skills.
Paths and resource symlinks must stay inside the package. Duplicate paths, duplicate
skill names and escaping resources fail loading. PEP 723 scripts do not declare skills.
A skill is discovered as <normalized-project-name>/<skill-name>, for example
vis-greeter/greeting. Its frontmatter name, or directory name when absent, uses
letters, digits, underscores and hyphens and starts with a letter or digit.
doc() identifies the package version and bundled resource directory.
Read it with doc("vis-greeter/greeting"), discover it with
apropos("vis-greeter/"), or request /skill:vis-greeter/greeting. An ordinary
skill with that exact qualified name takes precedence. Otherwise different packages
do not collide. The procedure does not change the working project or widen filesystem
access. Installing, listing or reading it never executes its instructions.
Code, skills and resources share the admitted source snapshot and the same reload, last-good retention and removal behavior. See Skills for authoring and precedence. Put tool reference material in docstrings rather than duplicating it here.
Leave a skill out of settings
Settings list each bundled skill under its extension, with its own switch. When the
skill is the main purpose of your extension, a second switch is not useful. Write the
skill's entry as a table with settings = false:
[tool.vis]
category = "tools"
skills = [{ path = "skills/browser", settings = false }, "skills/greeting"]
Settings then show no switch for skills/browser. The skill follows the extension's
Auto, On or Off choice: Off removes it, and Auto or On keep it. A value saved for the
skill earlier no longer applies. The other entry keeps its own switch. Vis versions
without this option reject the table form.
Standalone scripts
Keep a thin entry file under .vis/extensions/ and the implementation in its
own source tree. Declare both import roots and dependencies in a
PEP 723 script metadata block, before imports:
project/
vis.yml
einmal/src/einmal/__init__.py
.vis/extensions/einmal_tools.py
# .vis/extensions/einmal_tools.py
# /// script
# requires-python = ">=3.12"
# dependencies = ["httpx==0.28.1"]
# [tool.vis]
# source_paths = ["../../einmal/src"]
# ///
import blockether.vis.extension as vis
from einmal import status
def status_activity(*, phase, result, **_):
if phase != "success":
return None
return vis.ActivityPresentation("Check integration status", f"Status: {result[:160]}")
vis.register_extension(vis.Extension(
name="einmal",
description="Company tools.",
alias="einmal",
symbols=[vis.Symbol(
status, activity=vis.Activity(
label="Check integration status", show_start=False, render=status_activity
)
)],
))
This example checks local status and uses show_start=False. A status check that
waits for a network response should keep show_start=True. Package layout does
not replace the obligation to give every tool a human-readable
Activity presentation.
status must have a docstring, like any exported tool. The metadata is parsed
without executing the entry. Vis validates it, snapshots the source files,
installs dependencies, then evaluates the entry and registers its tools.
source_pathsnames import roots: directories containing the packages or modules you import, not the package directories themselves. Relative paths are resolved against the extension entry's directory, not the current working directory. Absolute paths are also accepted.- Each declared root's contents are merged into the frozen extension directory.
Missing directories, duplicate relative file names and roots containing the
extension directory are rejected. Use narrow source roots such as
src, not the whole checkout or a virtual environment. dependenciesaccepts standard package requirements, including version pins, extras and environment markers. Vis uses its bundled pip to install only wheels into~/.vis/python/packages. That directory is shared withpython_executionand other extensions. Vis does not change the project's.venv.- Set the index with
python.index_urlinvis.yml. Normal pip authentication and certificate settings still apply. A missing wheel or failed install is a load failure, not a fallback to source builds. requires-pythonchecks the embedded interpreter. Vis does not download another Python version to satisfy it./reloadtakes new source snapshots and resolves declared dependencies again for each extension that changed or failed. It skips unchanged extensions./reload --allloads every extension again. An unchanged loader scan does neither. Existing tools use source edits only after a reload. A failed reload keeps the last working extension.
Both embedded workers import shared installed dependencies. A tool call does not install them again. Each process has its own module cache. After package changes, reload to rebuild session workers. An already running call may keep its old imports.
See also
- Extension design — implementation and integration tests.
- Using an existing Python project — manual uv preparation.
- Extension troubleshooting — loading, imports and stale tools.
