mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 07:17:54 +08:00
4c597274e7
Session YOLO skips the soft confirms of `YOLO`-tagged tools — interpreter runs included — while untagged tools keep prompting and a command that escalates privileges keeps its own confirm through `DangerReason(yolo_skip=False)`. A plugin now also sees that answer shape where the danger classifier is documented, and the round's aggregated prompt leaves the covered calls out.
205 lines
7.1 KiB
Markdown
205 lines
7.1 KiB
Markdown
# Tool plugins
|
|
|
|
Third-party tools install as Python packages and register through the
|
|
**`plyngent.tools`** entry-point group. The host **allowlists** which plugins
|
|
load; default is **load none**.
|
|
|
|
Importing a plugin only **registers** tools into the process catalog. The CLI
|
|
still **selects** which names become model-visible (`surface=local` today).
|
|
|
|
## Layers (short)
|
|
|
|
```text
|
|
DEFINE+REGISTER @tool → ToolDefinition + catalog entry (source=plugin:…)
|
|
SELECT catalog.select(surface=…, …) → list[ToolDefinition]
|
|
EXECUTE ToolRegistry (confirm, instance/session bind, invoke)
|
|
```
|
|
|
|
| Layer | Question |
|
|
|--------|----------|
|
|
| Register | What tools exist in this process? |
|
|
| Select | Which may the model see this session? |
|
|
| Execute | How are they confirmed and run? |
|
|
|
|
Builtins use the same path with `source=builtin`. Plugins must not reuse a
|
|
builtin (or another plugin) **tool name** — registration fails with a
|
|
collision error.
|
|
|
|
## Author a plugin package
|
|
|
|
### 1. Define tools with `@tool`
|
|
|
|
```python
|
|
# acme_plyngent/tools.py
|
|
from plyngent.agent import ToolTag, tool
|
|
|
|
|
|
@tool(name="acme_ping", tags=ToolTag.LOCAL)
|
|
async def acme_ping() -> str:
|
|
"""Return a fixed pong (example plugin tool)."""
|
|
return "pong"
|
|
```
|
|
|
|
Notes:
|
|
|
|
- Prefer **`async def`** handlers (builtins are async-first).
|
|
- Default **`tags=ToolTag.LOCAL`**. Set at least one of `LOCAL` or `PUBLIC`.
|
|
- Workspace/exec tools should stay **`LOCAL`** unless reviewed for a shared host.
|
|
- Session-only helpers may add **`PUBLIC`** and **`SESSION_STATE`** when they
|
|
only touch session data and host-approved APIs (see plans under `doc/plans/`).
|
|
- Optional soft-confirm bits: **`YOLO`** (eligible for YOLO auto-approve),
|
|
**`TRUSTABLE`** (grant once per session after approve). The host's danger
|
|
classifier may refuse that skip for one call by returning
|
|
`DangerReason(text, yolo_skip=False)` — plyngent does so for a command that
|
|
escalates privileges. Hard denylists are never YOLO-skipped.
|
|
- Optional host-policy bit: **`READ_ONLY`** marks tools that never mutate host
|
|
state — they become eligible for read-only contexts such as
|
|
`/btw --tools=read` side turns or planning. Tools that only sometimes mutate
|
|
(e.g. `fetch` with a non-GET method) may still opt in; the handler must
|
|
enforce the constraint via the read-only context flag.
|
|
- Docstring becomes the model-facing description; parameter schemas come from
|
|
type hints.
|
|
- Use `register=False` only for unit tests that build a private registry.
|
|
|
|
### 2. Entry point
|
|
|
|
Call a zero-arg loader (or import a module) so `@tool` runs under the plugin
|
|
registration source:
|
|
|
|
```python
|
|
# acme_plyngent/__init__.py
|
|
def load() -> None:
|
|
"""Import tool modules so @tool registers into the process catalog."""
|
|
from acme_plyngent import tools as _tools
|
|
|
|
_ = _tools # registration side effects
|
|
```
|
|
|
|
Declare the entry point (PEP 621 / setuptools / hatch style):
|
|
|
|
```toml
|
|
# pyproject.toml of the plugin package
|
|
[project]
|
|
name = "acme-plyngent"
|
|
version = "0.1.0"
|
|
dependencies = [
|
|
"plyngent", # or pin a compatible range
|
|
]
|
|
|
|
[project.entry-points."plyngent.tools"]
|
|
acme = "acme_plyngent:load"
|
|
```
|
|
|
|
- Entry-point **name** (`acme`) is the **plugin id** used in config allowlists
|
|
and in catalog metadata (`plugin:acme`).
|
|
- Value is `module:attr`. If `attr` is callable, plyngent calls it after load;
|
|
if it is a module, import alone is enough when tools registered at import time.
|
|
|
|
Install the plugin into the **same environment** as `plyngent` (editable fine):
|
|
|
|
```bash
|
|
pdm add acme-plyngent
|
|
# or: pip install -e ./path/to/acme-plyngent
|
|
```
|
|
|
|
## Enable plugins in config
|
|
|
|
Plugins are **not** agent-only: allowlisting lives under a top-level **`[plugins]`**
|
|
section so future non-tool extensions can reuse the same gate.
|
|
|
|
In the user config (`plyngent config path`):
|
|
|
|
```toml
|
|
[plugins]
|
|
# Allowlist of entry-point names. Default empty = load no plugins.
|
|
enable = ["acme"]
|
|
# enable = ["*"] # every discovered plyngent.tools entry point
|
|
# Never load these names even if listed or matched by *:
|
|
# disable = ["legacy"]
|
|
```
|
|
|
|
| Setting | Meaning |
|
|
|---------|---------|
|
|
| `enable = []` / omitted | Load **no** plugins (safe default). |
|
|
| `enable = ["acme", "other"]` | Load only those entry-point names. |
|
|
| `enable = ["*"]` | Load all discovered plugins for the group. |
|
|
| `disable = ["x"]` | Skip plugin id `x` even when allowlisted or `*`. |
|
|
|
|
`enable` / `disable` are **plugin entry-point names** (e.g. `acme`), not individual
|
|
tool function names.
|
|
|
|
### CLI management
|
|
|
|
```bash
|
|
# Installed entry points + enable/disable status
|
|
plyngent plugins list
|
|
plyngent plugins list --config /path/to/plyngent.toml
|
|
|
|
# Allowlist / block (writes [plugins] and saves the file)
|
|
plyngent plugins enable acme
|
|
plyngent plugins enable '*' # load all discovered
|
|
plyngent plugins disable legacy
|
|
plyngent plugins undeny legacy # drop disable only
|
|
plyngent plugins clear --yes
|
|
```
|
|
|
|
In the chat REPL (writes config and rebuilds the tool registry when tools are on):
|
|
|
|
```text
|
|
/plugins # same as /plugins list
|
|
/plugins enable acme
|
|
/plugins disable legacy
|
|
/plugins undeny legacy
|
|
/plugins reload # re-read config + rebuild tools
|
|
/plugins clear
|
|
/tools --list # model-visible tools + catalog source
|
|
```
|
|
|
|
CLI load order (simplified):
|
|
|
|
1. `register_builtin_tools()`
|
|
2. `load_plugin_tools(plugins.enable, disable=plugins.disable)`
|
|
3. `catalog.select(surface="local")` → `ToolRegistry`
|
|
|
|
Listed tools show **tags** and **catalog source** (`builtin` vs `plugin:…`).
|
|
|
|
## Policy checklist for authors
|
|
|
|
1. Prefer **LOCAL** for host FS, shell, PTY, or arbitrary network side effects.
|
|
2. Do not shadow builtin names (`read_file`, `run_argv`, todo tools, …).
|
|
3. Do not rely on process-global workspace/todo alone when a host binds
|
|
instance/session context — prefer the same patterns as builtins
|
|
(`get_workspace_root` already prefers instance policy; session state via
|
|
context when you need it).
|
|
4. Soft danger: annotate with `YOLO` / `TRUSTABLE` only when the product
|
|
confirm story matches; hard denylists stay in workspace/command policy.
|
|
5. Keep tools **async**; raise ordinary exceptions or return `error: …` strings
|
|
consistently with builtins.
|
|
|
|
## Surfaces (local vs public)
|
|
|
|
| Catalog select | Included tools |
|
|
|----------------|----------------|
|
|
| `surface="local"` (CLI) | `LOCAL` and/or `PUBLIC` |
|
|
| `surface="public"` | `PUBLIC` only |
|
|
|
|
There is **no** multi-tenant public host in-tree yet; `surface=public` is
|
|
available for hosts that want a PUBLIC-only select (builtins: mainly the
|
|
todo series). Plugins should not set `PUBLIC` on workspace/exec tools by
|
|
default.
|
|
|
|
## API reference (code)
|
|
|
|
| Piece | Module |
|
|
|-------|--------|
|
|
| `@tool`, `ToolTag`, `ToolRegistry` | `plyngent.agent` / `plyngent.agent.tools` |
|
|
| Catalog, `ToolSource`, `select` | `plyngent.tools.catalog` |
|
|
| `load_plugin_tools` | `plyngent.tools.plugins` |
|
|
| Config fields | `PluginsConfig.enable`, `PluginsConfig.disable` (`[plugins]`) |
|
|
|
|
## Related docs
|
|
|
|
- [architecture.md](./architecture.md) — package layout
|
|
- [plyngent.example.toml](./plyngent.example.toml) — config sample
|
|
- Design notes (may lag code): [plans/](./plans/) — registration, tags, PUBLIC surface
|