Skip to main content

astra-plugin-sdk (Python)

Write a plugin for Astra in Python.

Repository: https://github.com/mihailinl/AstraPlugins

Installing

pip install "astra-plugin-sdk>=0.5,<0.6"
pip install "astra-plugin-sdk[test]"   # adds pytest, for the harness

That range is also what astra-plugin new writes into requirements.txt.

Take 0.5 or newer, and mean it. 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.

Python 3.10 or newer. Brings grpcio, grpcio-tools and protobuf.

astra-plugin test runs your plugin with whatever python is on PATH, so activate the virtualenv you installed into before running it.

To install from this repository instead — to try an unreleased change:

pip install ./AstraPlugins/astra-plugin-sdk-python

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::PluginClient and its auth interceptor rejects that identity on any gRPC path outside /astra.PluginHostService/, so every DaemonClient call answers permission_deniedclient = true included. host.send_chat_message is 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.host is a RecordingHost: .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 real Register handshake, 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_stream is implemented here and the daemon has no call site for TtsSynthesizeStream. ai_get_models is deprecated. Both are listed under "Findings" in the generated hook-parity page, which tools/parity renders from spec/hooks.yaml.
  • There is no chat_message_sync / on_chat_sync hook. That event was retired; a client plugin uses is_client() plus on_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

astra_plugin_sdk-0.6.1.tar.gz (242.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

astra_plugin_sdk-0.6.1-py3-none-any.whl (225.8 kB view details)

Uploaded Python 3

File details

Details for the file astra_plugin_sdk-0.6.1.tar.gz.

File metadata

  • Download URL: astra_plugin_sdk-0.6.1.tar.gz
  • Upload date:
  • Size: 242.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for astra_plugin_sdk-0.6.1.tar.gz
Algorithm Hash digest
SHA256 d455f2a5ed8454a800a14f5038697356b5da6a64f7f9ec4d191989413d7c7ad9
MD5 0dfd86ca60b209c0e3e6990e241a5fc8
BLAKE2b-256 e42212a8360c83730186ca842da068b1d4e844f938296e16c3352b837d022794

See more details on using hashes here.

Provenance

The following attestation bundles were made for astra_plugin_sdk-0.6.1.tar.gz:

Publisher: release-sdks.yml on mihailinl/AstraPlugins

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file astra_plugin_sdk-0.6.1-py3-none-any.whl.

File metadata

File hashes

Hashes for astra_plugin_sdk-0.6.1-py3-none-any.whl
Algorithm Hash digest
SHA256 b712977ee6c55a611ead81dd3ed5fe08732013e9b108bb79f92cfc8c0a64ccb7
MD5 5df96d8a1d2259e5eaaa9a9483aa6583
BLAKE2b-256 6247a03788a3ef0a9ef8ebd622d2dab62a42a34d8e9f1688d9298c952d5ed17d

See more details on using hashes here.

Provenance

The following attestation bundles were made for astra_plugin_sdk-0.6.1-py3-none-any.whl:

Publisher: release-sdks.yml on mihailinl/AstraPlugins

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.6.1 This release

2 files

0.6.0

2 files

0.5.0

2 files

0.4.0

2 files

0.2.0

2 files

0.1.4

2 files

0.1.3

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page