astra-plugin-sdk (Python)
Write a plugin for Astra in Python.
Repository: https://github.com/mihailinl/AstraPlugins
Installing today
Do not run pip install astra-plugin-sdk yet. PyPI has 0.4.0. The daemon
rejects every host RPC but Register without an x-session-token, and 0.5.0
is the first release that sends one — a 0.4.0 plugin starts, answers inbound
hooks, and gets unauthenticated on every log, fire_trigger and
set_variable it attempts. pip install "astra-plugin-sdk>=0.5" currently
fails outright with No matching distribution found.
Until 0.5.0 ships, install from this checkout — the same line CI runs:
git clone -b feat/plugin-production https://github.com/mihailinl/AstraPlugins
pip install ./AstraPlugins/astra-plugin-sdk-python
pip install pytest # or: pip install "./AstraPlugins/astra-plugin-sdk-python[test]"
python -c "import astra_plugin_sdk as s; print(s.__version__)" # must print 0.5.0
The branch matters. A bare
git clonechecks out the default branch, where this package is 0.4.0 — the version the paragraph above tells you not to use, installed from a path instead of from PyPI. Verified withgit show master:astra-plugin-sdk-python/pyproject.toml.feat/plugin-productionis not pushed yet (git ls-remote origin), so for now this means a local checkout of that branch; delete this note once it is the default.
Python 3.10 or newer. Brings grpcio, grpcio-tools and protobuf.
Astra ships no Python runtime. A Python plugin's plugin.toml declares
runtimes = ["python"] so the daemon fails with a clear message on a machine
with no interpreter, instead of dying at startup.
The smallest working plugin
from astra_plugin_sdk import Plugin, tool
class MyPlugin(Plugin):
@tool("Greet someone by name.")
async def hello(self, name: str, excited: bool = False):
return f"Hello, {name}{'!' if excited else '.'}"
if __name__ == "__main__":
MyPlugin().run()
astra-plugin new my-plugin --lang python writes this, a plugin.toml, a
pyproject.toml and a passing pytest suite.
The signature is the schema. @tool reads the method's type hints and
builds the JSON Schema the model is shown: name is required because it has no
default, excited is optional because it has one. There is no second place to
edit, so the schema and the handler cannot drift. Returning a plain value is
enough — the SDK wraps it as a successful tool result.
Returning dicts where a capability type is expected is deprecated and removed
in 0.7. Use the dataclasses: ToolDef, VoiceInfo, AiModelInfo, FieldDef,
DropdownOption, FieldCondition, ActionTypeDef, TriggerTypeDef,
UiContribution.
Actions, triggers, UI
from astra_plugin_sdk import Field, Plugin, action, tool
class DiceRoller(Plugin):
@action("Roll Dice", fields=[Field.text("notation", "Dice notation")])
async def roll_dice(self, params: dict):
return params.get("notation", "d20")
Also exported: trigger, and the six UI decorators ui_call, ui_page,
ui_slot, ui_effect, ui_overlay, ui_inject.
Talking back to Astra
class DiceRoller(Plugin):
@tool("Roll dice. Use for any request that involves dice or random numbers.")
async def roll(self, count: int, sides: int = 6):
await self.log_info(f"rolling {count}d{sides}")
await self.fire_trigger("on_roll_value", {"value": count * sides})
return f"{count} d{sides}"
self.log_info / log_warn / log_error, self.fire_trigger,
self.push_to_ui are the convenience wrappers; the full outbound surface is
self.host (HostClient): fire_trigger, log, get_config,
get_daemon_info, subscribe_events, push_to_ui, send_chat_message,
set_theme_contribution, set_variable.
Every one of those is default-deny. A manifest with no [permissions]
section may call Register, PluginLog, GetPluginSelfConfig and
GetDaemonInfo, and nothing else. fire_trigger needs
[permissions]
fire_trigger = { reason = "Fires the on_roll_value trigger so your commands can react to what you rolled" }
and the reason is what the user reads on the install consent sheet.
A plugin whose is_client() returns True also gets a DaemonClient — chat,
voice, commands, settings. Its "type a message as the user" method is
submit_user_message, not send_message.
That client does not reach anything yet. The daemon registers every plugin as
ClientType::PluginClientand its auth interceptor rejects that identity on any gRPC path outside/astra.PluginHostService/, so everyDaemonClientcall answerspermission_denied—client = trueincluded.host.send_chat_messageis the only working way to drive an AI turn. The daemon-side half is unbuilt; the SDK surface is here first.
Errors
from astra_plugin_sdk import NotConfigured
raise NotConfigured("api_key")
Eight exceptions with the same eight codes the Rust and TypeScript SDKs use:
BadArguments, NotFound, NotConfigured, Unauthorized, RateLimited,
Unavailable, Timeout, InternalError, all deriving from PluginError
(aliased ToolError / ActionError). Raise one from a tool, an action or a UI
call and the SDK fills in both halves of the response: the legacy error
string, byte-identical to what the Rust SDK produces, and the structured
error_detail carrying config_field, retry_after_ms and doc_url.
NotConfigured("api_key") is what turns "the tool failed" into a link to the
exact settings field.
Anything else a handler raises is adopted as INTERNAL, except ValueError /
TypeError (→ BAD_ARGUMENTS), KeyError (→ NOT_FOUND), TimeoutError and
PermissionError.
Note the 0.5.0 change: call_tool and execute_action no longer swallow
exceptions. An unknown tool raises NotFound instead of returning
{"success": False, ...}.
Testing
from astra_plugin_sdk.testing import Harness, fuzz_configs
from src.plugin import DiceRoller
def test_roll():
with Harness(DiceRoller()) as h:
assert h.tool_names() == ["roll"]
result = h.call_tool("roll", count=3, sides=6)
assert result.success, result.code
assert result.json == "3 d6"
assert len(h.host.fired_triggers()) == 1
assert h.host.logs()
def test_no_config_the_daemon_can_deliver_crashes_this_plugin():
with Harness(DiceRoller()) as h:
for payload in fuzz_configs():
h.set_config(payload)
Every block above was executed against this checkout before it was written down.
Harness— level 1. In process, no daemon, no socket, but through the real gRPC servicer, so a tool that is declared and not routed fails here.h.hostis aRecordingHost:.logs(),.fired_triggers(),.variables(),.ui_pushes(),.chat_messages(), and.fail_next(...)/.fail_always(...)to stage the refusal a user's[permissions]would produce.WireHarness/MockDaemon— level 2. A real gRPC server, a realRegisterhandshake, a real session token, real protobuf encoding.unauthenticated_calls()lists every host RPC that arrived without a valid token.
The fixtures register themselves through a pytest11 entry point, so
astra_harness, astra_wire, golden_pcm, wake_seed and fuzz_config are
usable with no conftest.py at all.
What this SDK does not do
- No isolation. Your plugin is a native process with the user's full privileges. Permissions constrain what the daemon will do for you; nothing constrains what your process does to the machine.
tts_synthesize_streamis implemented here and the daemon has no call site forTtsSynthesizeStream.ai_get_modelsis deprecated. Both are listed under "Findings" in the generated hook-parity page, whichtools/parityrenders fromspec/hooks.yaml.- There is no
chat_message_sync/on_chat_synchook. That event was retired; a client plugin usesis_client()pluson_conversation_event.
Full history, including everything breaking in 0.5.0:
CHANGELOG.md.
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file astra_plugin_sdk-0.5.0.tar.gz.
File metadata
- Download URL: astra_plugin_sdk-0.5.0.tar.gz
- Upload date:
- Size: 208.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b67c35f6bc3284c0037aee74ea742bad7525b58c4e1dae2a4eac94847daed992
|
|
| MD5 |
3939a5a3000146c11d725a9b6a79b28a
|
|
| BLAKE2b-256 |
e920b828c08a77c5440aae9f549e9074518b72ae038e57336c1f1553d34111a7
|
Provenance
The following attestation bundles were made for astra_plugin_sdk-0.5.0.tar.gz:
Publisher:
release-sdks.yml on mihailinl/AstraPlugins
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
astra_plugin_sdk-0.5.0.tar.gz -
Subject digest:
b67c35f6bc3284c0037aee74ea742bad7525b58c4e1dae2a4eac94847daed992 - Sigstore transparency entry: 2426789193
- Sigstore integration time:
-
Permalink:
mihailinl/AstraPlugins@7af4e5d700d86c6294007019ed8ca3a3ed057101 -
Branch / Tag:
refs/tags/sdk-v0.6.0 - Owner: https://github.com/mihailinl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-sdks.yml@7af4e5d700d86c6294007019ed8ca3a3ed057101 -
Trigger Event:
push
-
Statement type:
File details
Details for the file astra_plugin_sdk-0.5.0-py3-none-any.whl.
File metadata
- Download URL: astra_plugin_sdk-0.5.0-py3-none-any.whl
- Upload date:
- Size: 194.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
185330241e5f41e7340714feca28074031a5ff32ee50efc264968a14588782ae
|
|
| MD5 |
d8e0f2e0e33b9bccc6b85c1f1cdf723f
|
|
| BLAKE2b-256 |
7693d58f1697a11d10e11f4ed4d171f8d6fd222e3e660411ed40d0a130cc04c0
|
Provenance
The following attestation bundles were made for astra_plugin_sdk-0.5.0-py3-none-any.whl:
Publisher:
release-sdks.yml on mihailinl/AstraPlugins
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
astra_plugin_sdk-0.5.0-py3-none-any.whl -
Subject digest:
185330241e5f41e7340714feca28074031a5ff32ee50efc264968a14588782ae - Sigstore transparency entry: 2426789515
- Sigstore integration time:
-
Permalink:
mihailinl/AstraPlugins@7af4e5d700d86c6294007019ed8ca3a3ed057101 -
Branch / Tag:
refs/tags/sdk-v0.6.0 - Owner: https://github.com/mihailinl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release-sdks.yml@7af4e5d700d86c6294007019ed8ca3a3ed057101 -
Trigger Event:
push
-
Statement type: