Extensions and control
Local API methods for execution hosts, server control, notifications, integrations, plugins, diagnostics, and event subscriptions.
Extensions and control
The extension and control methods cover execution-host connections, server control, notifications, window titles, integrations, plugins, configuration diagnostics, and event subscriptions. Workspace, tab, agent, and pane methods are documented on Resources and Panes and agents. Canonical wire shapes live in the generated reference; Error semantics documents the error envelope, and transport and the request envelope are described in the Local API overview.
Trust boundary. The Local API is a local-user control surface. Any process that can open its socket can ask Gardn to stop or replace the server, write integration and plugin files, spawn plugin commands and panes as the current user, send content to a client, or hold a long-lived event stream. Treat the socket and every caller as trusted only when that is intended.
Methods
The schema defines these methods in the namespaces covered here:
ping.connection.list,connection.save,connection.delete,connection.test,connection.connect,connection.disconnect,connection.retire.start, andconnection.retire.status.server.stop,server.live_handoff,server.reload_config,server.agent_manifests, andserver.reload_agent_manifests.notification.show.client.window_title.setandclient.window_title.clear.integration.installandintegration.uninstall.plugin.link,plugin.list,plugin.unlink,plugin.enable,plugin.disable,plugin.action.list,plugin.action.invoke,plugin.log.list,plugin.pane.open,plugin.pane.focus, andplugin.pane.close.events.subscribeandevents.wait.
Execution-host connections
Connection profiles contain OpenSSH destination metadata. Authentication remains owned by system OpenSSH. Profile ids use ASCII letters, numbers, ., _, and -. The coordinator persists profiles in ~/.config/gardn/ssh-profiles.json.
ConnectionProfileInfo has these fields:
| Field | Type | Description |
|---|---|---|
profile_id | string | Stable profile id. |
name | string | Display name. |
target | string | Raw OpenSSH target. |
suggested_directory | string | null | Optional default path on the execution host. |
execution_host_id | string | Placement id, such as ssh:workbox:1. |
status | ConnectionStatusKind | disconnected, connecting, connected, reconnecting, disconnecting, authentication_required, or unavailable. |
error | string | null | Connection detail for reconnecting or unavailable. |
connection.list
Returns ConnectionList { profiles }. Each profile includes its current connection status.
{ "id": "connection-list", "method": "connection.list", "params": {} }connection.save
Creates or replaces a profile. Updating target increments the host-binding generation and changes execution_host_id. Updating only name or suggested_directory preserves the host id.
| Property | Type | Required | Description |
|---|---|---|---|
profile_id | string | yes | Stable profile id. |
name | string | yes | Non-empty display name. |
target | string | yes | Non-empty OpenSSH target. |
suggested_directory | string | null | no | Default remote path for resource creation. |
Returns ConnectionProfile { profile }.
Errors: connection_profile_invalid, connection_profile_save_failed.
{
"id": "connection-save",
"method": "connection.save",
"params": {
"profile_id": "workbox",
"name": "Work box",
"target": "alice@workbox",
"suggested_directory": "/srv/project"
}
}connection.delete
Removes an unreferenced profile by profile_id. Returns ConnectionDeleted { profile_id, removed }; removed is false when the id did not exist. The method fails while any live or persisted session, pending creation, or managed worker binding still references the profile's execution host.
Use the Settings removal workflow for cross-session inventory and cleanup. API automation must retire the execution host in every affected session before it calls connection.delete.
Errors: connection_profile_referenced, connection_profile_delete_failed.
connection.retire.start and connection.retire.status
These methods are per-session retirement primitives. They do not inventory other named sessions or delete the global profile. Use the Settings removal workflow unless the caller coordinates every affected session.
Both methods accept:
| Property | Type | Required | Description |
|---|---|---|---|
profile_id | string | yes | Saved profile id. |
execution_host_id | string | yes | Exact host id currently mapped by the profile. A stale or mismatched id fails closed. |
local_only | boolean | no | Default false. When true, forgets local runtime and termination records without claiming that remote cleanup occurred. |
connection.retire.start fences new work on the execution host, clears affected Group defaults, moves affected Workspace defaults to the coordinator's Local home directory, closes panes on that host, and persists the changed session before returning. It is idempotent after the host is fenced.
The response is ConnectionRetireStart { profile_id, execution_host_id, accepted, remaining_panes, remaining_terminals, pending_terminations }.
Poll connection.retire.status until ready is true. The response is ConnectionRetireStatus { profile_id, execution_host_id, ready, remaining_panes, remaining_terminals, pending_terminations }. A ready status also requests a cooperative worker shutdown when the worker is reachable.
After every affected session reports ready and owned worker bindings are removed, call connection.delete.
Errors include connection_profile_not_found, connection_retire_host_invalid, connection_retire_host_mismatch, execution_hosts_unavailable, and connection_retire_persist_failed.
{
"id": "connection-retire-start",
"method": "connection.retire.start",
"params": {
"profile_id": "workbox",
"execution_host_id": "ssh:workbox:1",
"local_only": false
}
}{
"id": "connection-retire-status",
"method": "connection.retire.status",
"params": {
"profile_id": "workbox",
"execution_host_id": "ssh:workbox:1",
"local_only": false
}
}connection.test, connection.connect, and connection.disconnect
Each method accepts { "profile_id": "..." } and queues the lifecycle action. The response is ConnectionActionQueued { profile, action }. action is test, connect, or disconnect. connection.connect automatically installs or selects the exact checksum-addressed worker required by this Gardn build. A busy incompatible worker drains without losing its runtimes; a compatible artifact is reused. The profile status in this immediate response is the status before the queued action is polled; call connection.list to observe the resulting status.
Errors: connection_profile_not_found.
ping
Returns the server version, client protocol number, and capability flags. server.live_handoff clients should inspect capabilities.live_handoff before attempting a handoff.
Params: PingParams, no properties.
Success: ResponseResult::Pong, discriminator "pong", with version: string, protocol: integer, and optional capabilities: ServerCapabilities. ServerCapabilities currently contains live_handoff: boolean.
{"id":"ping-1","method":"ping","params":{}}
{"id":"ping-1","result":{"type":"pong","version":"0.2.19","protocol":13,"capabilities":{"live_handoff":true}}}Server control
server.stop
Requests server shutdown. The application sets its quit flag and returns before the event loop exits. In headless mode, requests received after shutdown has begun are rejected with server_unavailable.
Params: EmptyParams, no properties.
Success: ResponseResult::Ok, discriminator "ok".
| Error | Meaning |
|---|---|
server_unavailable | A headless server is already shutting down, or the API cannot dispatch to the application. |
{"id":"stop-1","method":"server.stop","params":{}}
{"id":"stop-1","result":{"type":"ok"}}server.live_handoff
Replaces a running headless Unix server with another Gardn process while preserving the session snapshot and live pane runtimes. It is intended for an in-place update; it is not an ordinary restart.
Params: ServerLiveHandoffParams.
| Property | Type | Default | Description |
|---|---|---|---|
import_exe | string | null | null | Replacement executable. If omitted, the current executable path is used. |
expected_protocol | integer | null | null | If present, the replacement must speak this client protocol number. |
expected_version | string | null | null | If present, the replacement must report this Gardn version. |
Success: ResponseResult::Ok, discriminator "ok". The response is written before the old server exits; the connection then closes as part of the handoff.
Preconditions and execution boundary:
pingadvertisescapabilities.live_handoff = trueonly on Unix. Windows returnshandoff_failedwith"live handoff is only supported on Unix".- The interactive application handler does not perform a handoff and returns
unsupported_in_app_mode. - At most 64 pane file descriptors can be transferred in one handoff. The request fails before transfer when more are attached.
- The server creates a private handoff socket, disconnects clients and rejects pending client connections, pauses pane readers, captures the session and runtime state, starts the import process, transfers pane descriptors, waits for
restoredandready, then sendscommitted. The old server exits only after commit. A failure before commit kills the import process and restores the old server's readers and public sockets where possible. expected_protocolandexpected_versionare placed in the handoff manifest and validated by the replacement. A mismatch is ahandoff_failedresponse from the old server.- The API and client socket files are removed when the replacement takes ownership. The closed connection does not accept retries; clients must reconnect.
| Error | Meaning |
|---|---|
unsupported_in_app_mode | The request reached an interactive App, not the headless server. |
handoff_failed | Unix handoff could not bind its socket, exceeded the 64-pane limit, could not spawn the import process, could not pause or duplicate a pane runtime, failed token/manifest validation, failed runtime restoration, or the replacement did not become ready or commit. The message identifies the failed step. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"handoff-1","method":"server.live_handoff","params":{"expected_version":"0.2.19"}}
{"id":"handoff-1","result":{"type":"ok"}}server.reload_config
Loads config.toml and applies the live configuration without restarting the server. In headless mode, the server keybinding set is restored before the disk configuration is applied; this keeps server-owned keybindings distinct from a client's local keybindings.
Params: EmptyParams, no properties.
Success: ResponseResult::ConfigReload, discriminator "config_reload".
| Field | Type | Meaning |
|---|---|---|
status | "applied" | "partial" | "failed" | Whether the loaded configuration was fully applied. |
diagnostics | string[] | Human-readable load, parse, validation, or retained-value diagnostics. |
applied has an empty diagnostics array and clears the current configuration issue. partial means the file loaded but one or more values or sections were rejected; accepted values are applied and rejected values retain their previous settings. failed means the file could not be loaded or parsed into usable state; the previous configuration remains active. Diagnostic strings are display text, not a stable machine-readable sub-protocol. There is no config.* success discriminator.
| Error | Meaning |
|---|---|
serialization_error | Headless mode could not encode the reload result; this fallback error uses an empty id. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"reload-1","method":"server.reload_config","params":{}}
{"id":"reload-1","result":{"type":"config_reload","status":"partial","diagnostics":["ui.sidebar_min_width (40) is greater than sidebar_max_width (20); keeping previous [ui] settings"]}}server.agent_manifests
Returns the currently loaded agent-manifest summaries and the persisted remote-update status. It refreshes the App's summary view from its loaded manifest state; it does not reload manifest files. Use server.reload_agent_manifests to reload them.
Params: EmptyParams, no properties.
Success: ResponseResult::AgentManifestStatus, discriminator "agent_manifest_status".
| Field | Type | Default | Description |
|---|---|---|---|
last_check_unix | integer | null | omitted | Last remote manifest check time. |
last_result | string | null | omitted | Last remote update result. |
manifests | AgentManifestInfo[] | [] when none are loaded | One summary per agent. |
Each AgentManifestInfo contains agent, source, source_kind, local_override_shadowing_remote, and optional active_version, cached_remote_version, remote_update_result, remote_update_error, remote_last_checked_unix, and warning.
| Error | Meaning |
|---|---|
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"manifests-1","method":"server.agent_manifests","params":{}}
{"id":"manifests-1","result":{"type":"agent_manifest_status","manifests":[]}}server.reload_agent_manifests
Reloads agent manifests from disk, replaces the in-memory summaries, and resets all agent-detection runtimes so subsequent detection uses the new manifests.
Params: EmptyParams, no properties.
Success: ResponseResult::AgentManifestReload, discriminator "agent_manifest_reload", with manifests: AgentManifestInfo[].
| Error | Meaning |
|---|---|
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"manifests-2","method":"server.reload_agent_manifests","params":{}}
{"id":"manifests-2","result":{"type":"agent_manifest_reload","manifests":[]}}Notifications and window titles
These methods are client-facing. Headless mode sends terminal/system notifications and window-title commands to the current foreground client. With no foreground client, the request still succeeds with a negative result. Interactive App mode has no API foreground-client transport for window titles, so its title methods return no_foreground_client.
notification.show
Shows a notification using the configured toast.delivery mode.
Params: NotificationShowParams.
| Property | Type | Default | Description |
|---|---|---|---|
title | string | required | Control characters are removed, whitespace is collapsed, the value is trimmed, and the result is capped at 80 characters. An empty result is invalid. |
body | string | null | null | Sanitized like title, capped at 240 characters. A value that sanitizes to empty is treated as absent. |
position | ToastGardnPosition | null | null | Optional built-in toast position: top-left, top-right, bottom-left, or bottom-right. It has no effect for terminal/system delivery. |
sound | NotificationShowSound | "none" | "none", "done", or "request". Sound is played/forwarded only when local sound playback and the configured sound set allow it. |
Success: ResponseResult::NotificationShow, discriminator "notification_show", with shown: boolean and reason.
reason | shown | Meaning |
|---|---|---|
shown | true | A toast or foreground notification sink accepted the notification. |
disabled | false | toast.delivery is off. |
busy | false | Built-in Gardn delivery already has a toast. |
rate_limited | false | Another API notification was shown less than 250 ms ago. |
no_foreground_client | false | Terminal/system delivery has no foreground client, or its notification helper returned false/failed. |
Built-in gardn delivery checks busy before the rate limit, stores the toast in App state, and can play the requested sound. terminal and system delivery rate-limit first and then send to the foreground client (headless) or invoke the configured helper (interactive).
| Error | Meaning |
|---|---|
invalid_params | title is empty after sanitization ("notification title is empty"). |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"notify-1","method":"notification.show","params":{"title":"Build finished","body":"api workspace","position":"top-right","sound":"done"}}
{"id":"notify-1","result":{"type":"notification_show","shown":true,"reason":"shown"}}client.window_title.set
Sets the title of the current foreground client window.
Params: ClientWindowTitleSetParams.
| Property | Type | Default | Description |
|---|---|---|---|
title | string | required | Control characters including terminal title-control characters are removed, the value is trimmed, and the result is capped at 200 characters. An empty result is invalid. |
Success: ResponseResult::ClientWindowTitle, discriminator "client_window_title", with changed: boolean and reason: "set" or "no_foreground_client".
| Error | Meaning |
|---|---|
invalid_params | title is empty after sanitization ("window title is empty"). |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"title-1","method":"client.window_title.set","params":{"title":"api-refactor"}}
{"id":"title-1","result":{"type":"client_window_title","changed":true,"reason":"set"}}client.window_title.clear
Clears the title previously set through the API on the current foreground client.
Params: EmptyParams, no properties.
Success: ResponseResult::ClientWindowTitle, discriminator "client_window_title". A delivered clear is {"changed":true,"reason":"cleared"}; without a foreground client it is {"changed":false,"reason":"no_foreground_client"}.
It has no title to validate, so it does not return invalid_params.
{"id":"title-2","method":"client.window_title.clear","params":{}}
{"id":"title-2","result":{"type":"client_window_title","changed":true,"reason":"cleared"}}Integrations
Integration methods write or remove Gardn-managed hooks, extensions, plugins, and configuration entries in the selected agent's configuration area on the Coordinator Host. They run with the Local API process's user permissions. They do not target saved SSH Execution Hosts; use Settings > Integrations for remote integration management. Message strings describe paths and warnings and are not a stable machine-readable sub-protocol.
The IntegrationTarget values are pi, omp, claude, codex, copilot, devin, kimi, droid, opencode, hermes, qodercli, cursor, and grok.
On non-Windows platforms all targets are supported. On Windows all targets are supported. kimi is the only target with an agent-version probe: an old version fails, while an unavailable or unparsable version adds a warning and continues.
integration.install
Installs the selected target. Codex is special: the API calls the agent-profile installer, which installs into the default Codex home and every enabled Codex profile home that can be determined. A missing additional profile directory becomes a warning; if no Codex directory can be installed, the whole request fails.
Target installation writes these kinds of assets:
| Target | Installed asset reported in details.messages |
|---|---|
pi | Pi extension. |
omp | One or more Gardn extensions. |
claude | Claude hook and settings entry. |
codex | Codex hook plus Codex hooks/config entries, for the default and enabled profile homes. |
copilot | Copilot hook and settings entry. |
devin | Devin hook and settings entry. |
kimi | Kimi hook and config entry. |
droid | Droid hook and hooks entry. |
opencode | OpenCode plugin and TUI session plugin, with the TUI session plugin enabled in the TUI config. |
hermes | Hermes plugin directory and enabled config entry. |
qodercli | Qoder CLI hook and settings entry. |
cursor | Cursor hook and hooks entry. |
grok | Grok hook and hook config. |
Params: IntegrationInstallParams.
| Property | Type | Default | Description |
|---|---|---|---|
target | IntegrationTarget | required | One of the values above. |
Success: ResponseResult::IntegrationInstall, discriminator "integration_install", with target and details.messages: string[].
| Error | Meaning |
|---|---|
integration_install_failed | Unsupported platform, missing/old required agent, missing config directory, failed config edit, or another install I/O error. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"integration-1","method":"integration.install","params":{"target":"codex"}}
{"id":"integration-1","result":{"type":"integration_install","target":"codex","details":{"messages":["installed codex integration hook to /home/me/.codex/gardn-agent-state.sh"]}}}integration.uninstall
Removes Gardn-managed assets and entries for the selected target. It does not uninstall the agent and does not ask for confirmation. A target with nothing installed reports a message; it is not an error.
Params: IntegrationUninstallParams.
| Property | Type | Default | Description |
|---|---|---|---|
target | IntegrationTarget | required | One of the values above. |
The API handler calls the single-target uninstall path. In particular, unlike integration.install for Codex, API uninstall removes the default Codex installation only; it does not iterate enabled Codex profile homes.
Success: ResponseResult::IntegrationUninstall, discriminator "integration_uninstall", with target and details.messages: string[].
| Error | Meaning |
|---|---|
integration_uninstall_failed | Failed to remove a managed file or edit agent configuration. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"integration-2","method":"integration.uninstall","params":{"target":"claude"}}
{"id":"integration-2","result":{"type":"integration_uninstall","target":"claude","details":{"messages":["no claude hook found at /home/me/.claude/hooks/gardn-agent-state.sh"]}}}Plugins
Plugin commands are not sandboxed. A plugin action, event hook, build command when run by another surface, and plugin pane command can execute as the current user. Action commands use the plugin root as their working directory. Pane commands use the plugin root unless cwd overrides it. The API only links and exposes manifests; plugin.link does not run a manifest build command.
A manifest is loaded from gardn-plugin.toml. Plugin IDs are trimmed, non-empty, at most 120 characters, and contain only ASCII letters, digits, :, ., _, and -. Local action, pane, and link-handler IDs are trimmed, non-empty, at most 120 characters, and contain only ASCII letters, digits, :, _, and - (no dot). Platform values are linux, macos, and windows.
An installed plugin record contains plugin_id, name, version, min_gardn_version, optional description, manifest_path, plugin_root, enabled, optional plugin platforms, arrays build, actions, events, panes, and link_handlers, source, and warnings. A missing source manifest is retained with a warning beginning manifest unavailable: ; action listing skips it and action/pane execution rejects it.
plugin.link
Loads and registers one plugin manifest. If the ID is already registered, the new record replaces it after the manifest is validated. The registry write is rolled back in memory if persistence fails.
Params: PluginLinkParams.
| Property | Type | Default | Description |
|---|---|---|---|
path | string | required | Plugin directory or direct manifest path. |
enabled | boolean | true | Whether actions, event hooks, and panes may run. |
source | PluginSourceInfo | null | null | Optional provenance. kind is local (default) or github; optional fields are owner, repo, subdir, requested_ref, resolved_commit, managed_path, and installed_unix_ms. GitHub sources require a canonical managed_path under the managed plugin directory matching the plugin ID. |
Manifest validation also requires non-empty name, version, and semantic min_gardn_version, non-empty command argv, valid platform names, unique action/pane/link-handler IDs, valid link-handler regexes, and link handlers that reference an existing action. Unknown event names are retained as warnings; only known hook event kinds execute.
Success: ResponseResult::PluginLinked, discriminator "plugin_linked", with plugin: InstalledPluginInfo.
| Error | Meaning |
|---|---|
plugin_manifest_not_found | The path could not be canonicalized. |
invalid_plugin_manifest_path | The canonical manifest has no parent directory. |
plugin_manifest_read_failed | The manifest could not be read. |
plugin_manifest_parse_failed | TOML parsing failed. |
invalid_plugin_id, invalid_plugin_name, invalid_plugin_version, invalid_plugin_min_gardn_version | A required identity/version field is invalid. |
plugin_requires_newer_gardn | The manifest requires a newer Gardn release. |
invalid_plugin_platform | A platform name is unknown or platforms is an empty array. |
invalid_plugin_command | A command is empty or contains an empty argv string. |
invalid_plugin_action_id, invalid_plugin_action_title, invalid_plugin_pane_id, invalid_plugin_pane_title, invalid_plugin_event | A manifest member has an invalid ID, title, or event name. |
duplicate_plugin_action_id, duplicate_plugin_pane_id, duplicate_plugin_link_handler_id | A manifest contains duplicate IDs. |
invalid_plugin_link_handler_pattern, invalid_plugin_link_handler_action | A link-handler pattern or action reference is invalid. |
invalid_plugin_source | Supplied provenance is inconsistent or its managed path is invalid. |
plugin_registry_save_failed | plugins.json could not be written; the previous record is restored. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"link-1","method":"plugin.link","params":{"path":"/home/me/plugins/workspace-tools"}}
{"id":"link-1","result":{"type":"plugin_linked","plugin":{"plugin_id":"workspace-tools","name":"Workspace Tools","version":"0.1.0","min_gardn_version":"0.2.0","manifest_path":"/home/me/plugins/workspace-tools/gardn-plugin.toml","plugin_root":"/home/me/plugins/workspace-tools","enabled":true,"source":{"kind":"local"},"build":[],"actions":[],"events":[],"panes":[],"link_handlers":[],"warnings":[]}}}plugin.list
Lists registered plugins in ascending plugin_id order. A manifest-unavailable record remains listable with its warning. A filter that matches no plugin returns an empty array.
Params: PluginListParams.
| Property | Type | Default | Description |
|---|---|---|---|
plugin_id | string | null | null | Optional normalized plugin ID filter. |
Success: ResponseResult::PluginList, discriminator "plugin_list", with plugins: InstalledPluginInfo[].
| Error | Meaning |
|---|---|
invalid_plugin_id | A supplied filter is not a valid plugin ID. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"list-1","method":"plugin.list","params":{}}
{"id":"list-1","result":{"type":"plugin_list","plugins":[]}}plugin.unlink
Removes a plugin record from the registry. It does not delete the plugin directory. Existing plugin panes keep running as ordinary panes; the plugin attribution records are removed, so those panes can no longer be focused or closed through the plugin pane methods.
Params: PluginUnlinkParams, with plugin_id: string.
Success: ResponseResult::PluginUnlinked, discriminator "plugin_unlinked", with plugin_id and removed: boolean. An unknown but valid ID succeeds with removed: false.
| Error | Meaning |
|---|---|
invalid_plugin_id | The ID is invalid. |
plugin_registry_save_failed | Registry persistence failed; the plugin record and pane attribution are restored. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"unlink-1","method":"plugin.unlink","params":{"plugin_id":"workspace-tools"}}
{"id":"unlink-1","result":{"type":"plugin_unlinked","plugin_id":"workspace-tools","removed":true}}plugin.enable
Sets a registered plugin's enabled flag to true. The flag is persisted. Enabling does not run actions, hooks, or panes immediately.
Params: PluginSetEnabledParams, with plugin_id: string.
Success: ResponseResult::PluginEnabled, discriminator "plugin_enabled", with the updated plugin: InstalledPluginInfo.
| Error | Meaning |
|---|---|
invalid_plugin_id | The ID is invalid. |
plugin_not_found | No registered plugin has that ID. |
plugin_registry_save_failed | Persistence failed; the previous flag is restored. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
plugin.disable
Sets a registered plugin's enabled flag to false. The plugin remains registered and listable; action invocation, event hooks, and pane opening are rejected until it is enabled again. Existing panes are not automatically closed.
Params: PluginSetEnabledParams, with plugin_id: string.
Success: ResponseResult::PluginDisabled, discriminator "plugin_disabled", with the updated plugin: InstalledPluginInfo.
Errors are the same as plugin.enable.
{"id":"disable-1","method":"plugin.disable","params":{"plugin_id":"workspace-tools"}}
{"id":"disable-1","result":{"type":"plugin_disabled","plugin":{"plugin_id":"workspace-tools","name":"Workspace Tools","version":"0.1.0","min_gardn_version":"0.2.0","manifest_path":"/home/me/plugins/workspace-tools/gardn-plugin.toml","plugin_root":"/home/me/plugins/workspace-tools","enabled":false,"source":{"kind":"local"},"build":[],"actions":[],"events":[],"panes":[],"link_handlers":[],"warnings":[]}}}plugin.action.list
Lists actions from available manifests, sorted by qualified ID (plugin_id.action_id). Disabled plugins are included; only unavailable manifests are skipped.
Params: PluginActionListParams.
| Property | Type | Default | Description |
|---|---|---|---|
plugin_id | string | null | null | Optional normalized plugin ID filter. |
Success: ResponseResult::PluginActionList, discriminator "plugin_action_list", with actions: PluginActionInfo[]. Each action has plugin_id, action_id, title, optional description, contexts (global, workspace, tab, pane, selection), command, and optional platforms.
| Error | Meaning |
|---|---|
invalid_plugin_id | A supplied filter is invalid. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"actions-1","method":"plugin.action.list","params":{"plugin_id":"workspace-tools"}}
{"id":"actions-1","result":{"type":"plugin_action_list","actions":[]}}plugin.action.invoke
Starts a manifest action asynchronously as the current user, with the plugin root as the process working directory. The response returns while the command is running; completion is reflected later by plugin.log.list.
Params: PluginActionInvokeParams.
| Property | Type | Default | Description |
|---|---|---|---|
action_id | string | required | With plugin_id, a local action ID. Without plugin_id, either a local ID or a qualified plugin_id.action_id. |
plugin_id | string | null | null | Restricts lookup to one plugin. |
context | PluginInvocationContext | null | null | Optional caller context merged over server-derived active workspace/tab/focused-pane context. |
With plugin_id, the plugin and local action ID are normalized and looked up directly. Without it, the trimmed action string is matched against local and qualified IDs: no match is plugin_action_not_found, and multiple matches are ambiguous_plugin_action. Caller-supplied non-null context fields take precedence; the request ID supplies correlation_id when none is supplied. Context fields are optional: workspace_id, workspace_label, workspace_cwd, tab_id, tab_label, focused_pane_id, focused_pane_cwd, focused_pane_agent, focused_pane_status, selected_text, invocation_source, correlation_id, clicked_url, and link_handler_id.
Execution requires an available manifest, enabled: true, and an effective platform list that includes the host. An action-level platforms list overrides the plugin-level list. The command receives protected GARDN_* plugin environment variables, including the socket path, plugin root/config/state directories, plugin ID, action ID, context JSON, and current workspace/tab/pane IDs when present. At most 32 plugin commands run concurrently. Stdout and stderr are captured after completion and capped at the last 64 KiB each.
Success: ResponseResult::PluginActionInvoked, discriminator "plugin_action_invoked", with action: PluginActionInfo, merged context, and log: PluginCommandLogInfo. The initial log has status: "running", finished_unix_ms: null, exit_code: null, stdout: null, stderr: null, and error: null.
| Error | Meaning |
|---|---|
invalid_plugin_id | Supplied plugin_id is invalid. |
invalid_plugin_action_id | With plugin_id, action_id is not a valid local ID. |
plugin_not_found | The requested plugin is not registered. |
plugin_manifest_unavailable | The selected plugin's manifest is unavailable. |
plugin_action_not_found | No action matches. |
ambiguous_plugin_action | A local action ID matches more than one plugin; include plugin_id. |
plugin_disabled | The plugin is disabled. |
platform_unsupported | The effective action/plugin platform list excludes the host. |
invalid_plugin_command | The command is empty or contains an empty argv string. |
invalid_plugin_context | The merged context could not be serialized. |
plugin_command_limit_reached | 32 commands are already running. A failed log record is added for the attempt. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"invoke-1","method":"plugin.action.invoke","params":{"plugin_id":"workspace-tools","action_id":"bootstrap","context":{"invocation_source":"api"}}}
{"id":"invoke-1","result":{"type":"plugin_action_invoked","action":{"plugin_id":"workspace-tools","action_id":"bootstrap","title":"Bootstrap","contexts":["workspace"],"command":["./bootstrap"]},"context":{"invocation_source":"api","correlation_id":"invoke-1"},"log":{"log_id":"plugin-log-1","plugin_id":"workspace-tools","action_id":"bootstrap","command":["./bootstrap"],"status":"running","started_unix_ms":1700000000000}}}plugin.log.list
Returns recent plugin command records. The in-memory log retains at most 200 records for the session. Results contain the newest selected window in chronological order (oldest first within that window).
Params: PluginLogListParams.
| Property | Type | Default | Description |
|---|---|---|---|
plugin_id | string | null | null | Optional normalized plugin ID filter. |
limit | integer | null | 50 | Clamped to 1 through 200. |
Each PluginCommandLogInfo contains log_id, plugin_id, optional action_id, optional event name event, command, status (running, succeeded, or failed), started_unix_ms, optional finished_unix_ms, exit_code, stdout, stderr, and error. Event-hook records set event and leave action_id absent. Spawn failures have no exit code; output fields are empty strings after a failed spawn. A limit-reached record has error set.
Success: ResponseResult::PluginLogList, discriminator "plugin_log_list", with logs: PluginCommandLogInfo[].
| Error | Meaning |
|---|---|
invalid_plugin_id | A supplied filter is invalid. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"logs-1","method":"plugin.log.list","params":{"plugin_id":"workspace-tools","limit":10}}
{"id":"logs-1","result":{"type":"plugin_log_list","logs":[]}}plugin.pane.open
Opens a pane declared by a plugin manifest. The process runs as the current user. Split, tab, and zoomed placements create ordinary session panes; overlay placement creates a detached popup runtime owned by the requesting client view. The server records plugin attribution and emits pane.created (and tab.created for tab placement) before returning the success response.
Params: PluginPaneOpenParams.
| Property | Type | Default | Description |
|---|---|---|---|
plugin_id | string | required | Normalized plugin ID. |
entrypoint | string | required | Local pane ID declared in the manifest. |
placement | PluginPanePlacement | null | manifest placement, otherwise overlay | overlay, split, tab, or zoomed. |
workspace_id | string | null | null | Only used by tab; selects the workspace for a new tab. |
target_pane_id | string | null | current public pane for split/zoomed | Existing pane to split. |
direction | SplitDirection | null | right for split/zoomed | right or down. |
cwd | string | null | plugin root | Working directory override. |
focus | boolean | false | Focuses split/tab panes when true. Zoomed placement always focuses. Overlay placement ignores this field. |
env | object | {} | Extra environment entries. Keys may not be empty, contain =, or contain NUL; values may not contain NUL. |
Placement constraints:
| Placement | Allowed targeting fields and behavior |
|---|---|
overlay | Rejects workspace_id, target_pane_id, and direction; opens against the requesting client's active workspace as a client-owned detached popup. |
split | Rejects workspace_id; uses target_pane_id or the current public pane, and defaults direction to right. |
zoomed | Same target and direction rules as split, then marks the tab zoomed and focuses it. |
tab | Allows workspace_id but rejects target_pane_id and direction; creates a tab in the named workspace or active workspace. |
The plugin manifest's pane platform list overrides its plugin platform list. The launch environment removes caller attempts to set protected GARDN_* keys (PLUGIN_ID, PLUGIN_ROOT, PLUGIN_CONFIG_DIR, PLUGIN_STATE_DIR, PLUGIN_ENTRYPOINT_ID, PLUGIN_CONTEXT_JSON, and BIN_PATH) and replaces them with server values. The environment also includes the pane context JSON and entrypoint ID.
Only the client view that opened an overlay popup can see, focus, or close it through these methods. A second overlay request from that client replaces its current popup. The popup closes and its runtime and plugin attribution are removed when the owner closes it, disconnects, or the popup process exits. Other attached clients receive plugin_pane_not_found for that popup id. These ownership rules do not apply to split, tab, or zoomed plugin panes.
Success: ResponseResult::PluginPaneOpened, discriminator "plugin_pane_opened", with plugin_pane: PluginPaneInfo containing plugin_id, entrypoint, and the standard pane: PaneInfo descriptor.
| Error | Meaning |
|---|---|
invalid_plugin_id, plugin_not_found, plugin_manifest_unavailable, plugin_disabled | Plugin lookup or lifecycle gate failed. |
invalid_plugin_entrypoint | Entrypoint is not a valid local identifier. |
plugin_pane_not_found | Entrypoint is not declared by the manifest. |
platform_unsupported | Effective pane/plugin platform list excludes the host. |
invalid_params | Fields conflict with placement. |
no_active_pane | Split/zoomed omitted target_pane_id and has no current pane. |
pane_not_found | A supplied or resolved split/zoomed target does not exist. |
no_active_workspace | Tab placement omitted workspace_id and has no active workspace. |
workspace_not_found | A supplied tab workspace does not exist. |
plugin_pane_open_failed | Pane process spawn failed or the pane disappeared before response encoding. |
invalid_plugin_context | Pane context could not be serialized. |
invalid_env | An environment key/value violates the launch environment rules. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"pane-1","method":"plugin.pane.open","params":{"plugin_id":"workspace-tools","entrypoint":"log-tail","placement":"split","target_pane_id":"w1943cd2b11a1:p1","direction":"right","focus":true}}
{"id":"pane-1","result":{"type":"plugin_pane_opened","plugin_pane":{"plugin_id":"workspace-tools","entrypoint":"log-tail","pane":{"pane_id":"w1943cd2b11a1:p2","terminal_id":"term_1943cd2b22b2","workspace_id":"w1943cd2b11a1","tab_id":"w1943cd2b11a1:t1","focused":true,"agent_status":"unknown","state_labels":{},"tokens":{},"revision":0}}}}plugin.pane.focus
Focuses a pane currently attributed to a plugin and switches the requesting client view to terminal mode. For an overlay popup, the requester must be its owning client; another attached client receives plugin_pane_not_found.
Params: PluginPaneFocusParams, with pane_id: string.
Success: ResponseResult::PluginPaneFocused, discriminator "plugin_pane_focused", with the attributed plugin_pane record.
| Error | Meaning |
|---|---|
plugin_pane_not_found | The ID is not a current attributed plugin pane, or it is an overlay owned by another client view. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"pane-focus-1","method":"plugin.pane.focus","params":{"pane_id":"w1943cd2b11a1:p2"}}
{"id":"pane-focus-1","result":{"type":"plugin_pane_focused","plugin_pane":{"plugin_id":"workspace-tools","entrypoint":"log-tail","pane":{"pane_id":"w1943cd2b11a1:p2","terminal_id":"term_1943cd2b22b2","workspace_id":"w1943cd2b11a1","tab_id":"w1943cd2b11a1:t1","focused":true,"agent_status":"unknown","state_labels":{},"tokens":{},"revision":0}}}}plugin.pane.close
Closes a pane attributed to a plugin and removes its attribution. Overlay close is owner-only and tears down that detached popup runtime directly. Other placements use the standard pane-close path, so they can also return standard close errors such as confirmation_required when the configured close policy requires confirmation.
Params: PluginPaneCloseParams, with pane_id: string.
Success: ResponseResult::PluginPaneClosed, discriminator "plugin_pane_closed", with pane_id only.
| Error | Meaning |
|---|---|
plugin_pane_not_found | The ID is not a current attributed plugin pane, is an overlay owned by another client view, or is not the requester's active popup. |
pane_not_found | The underlying pane disappeared. |
confirmation_required | The standard close path requires confirmation for an implicit group close. |
server_unavailable | Shutdown was already in progress or the request could not be dispatched. |
{"id":"pane-close-1","method":"plugin.pane.close","params":{"pane_id":"w1943cd2b11a1:p2"}}
{"id":"pane-close-1","result":{"type":"plugin_pane_closed","pane_id":"w1943cd2b11a1:p2"}}Event subscriptions
events.subscribe
Opens the only Local API method that keeps its socket open for a stream. The server validates every subscription, including pane probes and regex compilation, before sending an acknowledgement. subscriptions may mix plain event subscriptions and stateful pane subscriptions. An empty array is accepted and produces an idle stream.
Params: EventsSubscribeParams.
| Property | Type | Default | Description |
|---|---|---|---|
subscriptions | Subscription[] | required | Tagged entries whose type selects a subscription kind. |
Acknowledgement: ResponseResult::SubscriptionStarted, discriminator "subscription_started". Exactly one acknowledgement line is written before normal event lines:
{ "id": "sub-1", "result": { "type": "subscription_started" } }Event lines after the acknowledgement do not carry an id.
The stream polls every 100 ms. Event writes have a five-second socket write timeout. It remains open until the client closes it, the server stops, or a write/disconnect failure ends it. Unix clients are probed for disconnect between polls; Windows relies on writes to detect disconnects. There are no durable offsets. Plain events use a bounded in-memory history of the most recent 512 events, and a new plain subscription starts at sequence zero, so retained matching history may be emitted immediately. Stateful pane subscriptions poll current pane state and do not replay an old session. Reconnect and reconcile with a fresh workspace.list, tab.list, or pane.list snapshot when loss matters.
A validation error is sent without an acknowledgement and the connection closes. A post-ack polling/encoding error is sent with the original request id, then the connection closes. Malformed JSON or a malformed subscriptions value is invalid_request; a bad regex is invalid_regex; a pane probe can return pane_not_found; and a response/event decode or encode failure is internal_error.
Plain event subscription kinds
These entries have no fields other than type. Their emitted line is an EventEnvelope with event serialized as the snake_case EventKind value and data.type carrying the same discriminator.
Subscription type | data payload |
|---|---|
workspace.created | type: "workspace_created", workspace: WorkspaceInfo. |
workspace.updated | type: "workspace_updated", workspace: WorkspaceInfo. |
workspace.renamed | type: "workspace_renamed", workspace_id, label. |
workspace.closed | type: "workspace_closed", workspace_id, optional workspace: WorkspaceInfo. |
workspace.focused | type: "workspace_focused", workspace_id. |
tab.created | type: "tab_created", tab: TabInfo. |
tab.closed | type: "tab_closed", tab_id, workspace_id. |
tab.renamed | type: "tab_renamed", tab_id, workspace_id, label. |
tab.focused | type: "tab_focused", tab_id, workspace_id. |
pane.created | type: "pane_created", pane: PaneInfo. |
pane.closed | type: "pane_closed", pane_id, workspace_id. |
pane.focused | type: "pane_focused", pane_id, workspace_id. |
pane.moved | type: "pane_moved", previous_pane_id, previous_workspace_id, previous_tab_id, pane: PaneInfo, optional created_workspace, created_tab, closed_workspace_id, and closed_tab_id. |
pane.exited | type: "pane_exited", pane_id, workspace_id. |
pane.agent_detected | type: "pane_agent_detected", pane_id, workspace_id, optional agent. |
layout.updated | type: "layout_updated", layout: PaneLayoutSnapshot. |
A plain event example:
{
"event": "workspace_focused",
"data": { "type": "workspace_focused", "workspace_id": "w1943cd2b11a1" }
}pane.output_changed is an EventKind used in the general event model and by events.wait matching. It is not a valid events.subscribe type.
Stateful pane subscriptions
These entries perform a validation probe before the acknowledgement and poll the pane every cycle.
type | Required fields | Optional/default fields | Emission |
|---|---|---|---|
pane.output_matched | pane_id, source, match | lines: integer | null; strip_ansi: true | Emits on the transition from no match to a matching line. It emits again only after a poll finds no match and a later poll matches. |
pane.agent_status_changed | pane_id | agent_status: AgentStatus | null | Emits on status or presentation changes. Presentation includes title, display agent, custom status, state labels, and tokens. With a filter, the initial current state emits once when it already matches. |
pane.scroll_changed | pane_id | none | Emits when the pane scroll snapshot changes; the validation snapshot is the baseline. |
source is visible, recent, recent_unwrapped, or detection. For pane.output_matched, the validation probe uses the supplied source, while polling maps recent to recent_unwrapped. lines is passed to the pane read operation; the underlying read defaults to 80 lines and caps the value at 1000. match is either {"type":"substring","value":"..."} or {"type":"regex","value":"..."}. Substring matching checks whether any text line contains the value. Regex matching uses Rust regular expressions and is compiled during validation; an invalid pattern returns invalid_regex before acknowledgement. strip_ansi defaults to true.
Stateful event envelopes use the dotted subscription name and untagged data selected by event:
{"event":"pane.output_matched","data":{"pane_id":"w1943cd2b11a1:p1","matched_line":"build finished","read":{"pane_id":"w1943cd2b11a1:p1","workspace_id":"w1943cd2b11a1","tab_id":"w1943cd2b11a1:t1","source":"recent_unwrapped","format":"text","text":"build finished\n","revision":0,"truncated":false}}}
{"event":"pane.agent_status_changed","data":{"pane_id":"w1943cd2b11a1:p1","workspace_id":"w1943cd2b11a1","agent_status":"done","agent":"codex","state_labels":{},"tokens":{}}}
{"event":"pane.scroll_changed","data":{"pane_id":"w1943cd2b11a1:p1","workspace_id":"w1943cd2b11a1","scroll":{"offset_from_bottom":12,"max_offset_from_bottom":128,"viewport_rows":48}}}A combined request and acknowledgement:
{"id":"sub-2","method":"events.subscribe","params":{"subscriptions":[{"type":"workspace.created"},{"type":"pane.output_matched","pane_id":"w1943cd2b11a1:p1","source":"recent","lines":20,"match":{"type":"substring","value":"ready"}}]}}
{"id":"sub-2","result":{"type":"subscription_started"}}A pane validation failure has no acknowledgement:
{"id":"sub-bad","method":"events.subscribe","params":{"subscriptions":[{"type":"pane.scroll_changed","pane_id":"p_missing"}]}}
{"id":"sub-bad","error":{"code":"pane_not_found","message":"pane p_missing not found"}}events.wait
The schema defines events.wait with a one-shot EventsWaitParams object. The current API dispatch has no handler for it; the method returns not_implemented and timeout_ms does not make it wait.
Params: EventsWaitParams.
| Property | Type | Default | Description |
|---|---|---|---|
match_event | EventMatch | required | Tagged by event; the supported schema variants are listed below. |
timeout_ms | integer | null | null | Intended timeout field; currently unused because the method is not implemented. |
EventMatch variants are:
event | Fields |
|---|---|
workspace_created | optional workspace_id. |
workspace_updated | workspace_id. |
workspace_closed | workspace_id. |
workspace_renamed | workspace_id, optional label. |
workspace_focused | workspace_id. |
tab_created | optional tab_id, optional workspace_id. |
tab_closed | tab_id. |
tab_renamed | tab_id, optional label. |
tab_focused | tab_id. |
pane_created | optional pane_id, optional workspace_id. |
pane_closed | pane_id. |
pane_focused | pane_id. |
pane_moved | pane_id. |
pane_output_changed | pane_id, optional min_revision. |
pane_exited | pane_id. |
pane_agent_detected | pane_id, optional agent. |
pane_agent_status_changed | pane_id, agent_status. |
Current result: no success discriminator is produced. The current response is:
{"id":"wait-1","method":"events.wait","params":{"match_event":{"event":"workspace_created"},"timeout_ms":1000}}
{"id":"wait-1","error":{"code":"not_implemented","message":"method not implemented yet"}}Last updated on