Plugin Development and Debugging

Use this runbook to bring up the local plugin stack, configure a device-local provider, trace lifecycle failures, and add new catalog capabilities without weakening the local-first boundary.

Local Environment

Run the regular service stack and the Desktop/Runner from the same checkout:

just dev
just desktop-dev

The local origin is https://scilaxy.local. Do not run full Compose stacks from two worktrees at the same time; the stack, database, Redis, ports, and Desktop connection are shared resources.

just dev provisions the Dev plugin authority before starting the service stack. desktop/scripts/provision-dev-plugin-runner.mjs creates or reuses ignored local material:

  • the Dev server's Ed25519 signing key;
  • the plugin-secret encryption keyring;
  • the public trust resource under the Dev channel home.

just desktop-dev generates and stages the runtime manifest, seeds the local runtime cache, builds Runner, and starts Electron. API, Web, and Worker hot-reload under just dev; ordinary source edits do not require rebuilding the entire stack.

Before diagnosing a plugin, confirm:

  1. Desktop reports channel dev and backend origin https://scilaxy.local.
  2. The trust resource resolves the exact WSS origin and /scilaxy/ws/v1/runner/device path.
  3. The Desktop enrollment is active, its lease expiry keeps advancing, and its catalog digest is present.
  4. The installation and selected device belong to the same authenticated user.

Configure a Local Profile

For Feishu or GitHub, connection setup must complete before Enable:

  1. Install the plugin and select an online Desktop.
  2. Enter a profile label. After trimming it must contain 1–128 Unicode code points and only letters, digits, spaces, ., _, or -.
  3. Select Continue on Desktop to create an authorize_local_profile device action.
  4. Complete the isolated Electron prompt. Feishu asks for cn or global, App ID, and App Secret.
  5. Wait for the connection to become ready and its local provider probe to become healthy.
  6. Review and save the requested plugin permissions.
  7. Enable the installation.

The connection and permission steps may be completed in either order, but both must exist. If the profile label is empty, the Desktop continuation button is intentionally disabled.

Package Contract

Every portable package contains a root plugin.json conforming to Agent Plugins 1.0.0. Skills are discovered from skills/<name>/SKILL.md; an optional root mcp.json declares MCP servers. SciLaxy-specific local execution is declared under extensions["ai.scilaxy"].

A minimal local-provider manifest has this shape:

{
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
  "name": "org.example.local-provider",
  "version": "1.0.0",
  "author": { "name": "Example Publisher" },
  "extensions": {
    "ai.scilaxy": {
      "schema": "https://scilaxy.ai/schemas/plugin-extension/1.0.0",
      "localProviders": [
        {
          "id": "example-local",
          "provider": "example",
          "toolCatalogDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
          "protocol": "plugin-provider/1",
          "transport": "stdio",
          "entrypoint": {
            "path": "bin/provider",
            "args": [],
            "workingDirectory": "."
          },
          "connectionModes": ["device_local"]
        }
      ]
    }
  }
}

Replace the example tool-catalog digest with the actual lowercase SHA-256. The extension is a closed schema: localProviders may define package-relative stdio providers; inboundTransports may define provider WebSocket or acknowledged webhook relay; channels currently use the supported Feishu/GitHub contracts; agentRuntimes are builtin-source only and limited to supported adapters.

Validation is deterministic and offline—it does not execute code or fetch remote schemas. The package must contain only regular files, normalized safe relative paths, no links/devices/sockets, no credential-bearing files, at most 2,000 files and 64 MiB uncompressed data, path depth at most 32, and compression ratio at most 100:1. Non-bundled local providers also require explicit host-authority consent on the selected device.

API Surface

Authenticated plugin REST routes use /scilaxy/api/v1/plugins:

AreaCanonical routes
CatalogGET /catalog; GET /catalog/:plugin_key/releases/:version
InstallationGET/POST /installations; GET/DELETE /installations/:installation_id
LifecyclePOST /installations/:id/{update,enable,disable,repair}; GET /installations/:id/operations/:operation_id
ObservabilityGET /installations/:id/health; GET /installations/:id/activity
ConnectionGET/POST /installations/:id/connections; detail GET/PATCH/DELETE; authorize/verify/revoke
PermissionGET/PUT /installations/:id/permissions
DeviceGET /devices; enrollment, activate, challenge, lease, ticket, revoke
Device actionPOST/GET /device-actions; claim, awaiting-confirmation, challenge, complete, fail, cancel
EffectGET /effects/:effect_id; decisions use the shared tool-approval API
OAuth callbackGET /oauth/:provider/:region/:redirect_id/callback

The Runner channel is WSS /scilaxy/ws/v1/runner/device. GitHub public ingress is POST /scilaxy/api/v1/plugin-webhooks/github/:endpoint_token.

Canonical REST paths have no trailing slash. Source-of-truth request and response types live in service/pkg/web/views/plugins/, plugindevices/, and runnerws/.

Readiness Failures

Enable submits an asynchronous lifecycle command. The API can accept the command even when the Worker later rejects it. Always read both the latest plugin_installation_operation and the installation health blockers.

BlockerMeaningRecovery
package_unavailableThe selected release package is unavailable or unverifiedRestore or republish the exact release
connection_requiredA declared provider has no usable connectionCreate a local profile or complete OAuth
reauthentication_requiredConnection exists but its credential is no longer validReauthorize on the bound device
permission_requiredNo current permission document covers the capabilityReview and save permissions
device_offlineThe bound device has no valid leaseStart that Desktop/Runner and renew; do not switch devices silently
runtime_incompatibleAgent Runtime config, protocol, or version is incompatibleRepair or reconfigure the runtime on the target device
release_revokedThe release is quarantined or revokedUpdate to an available release
policy_changedDeclaration, config digest, or policy can no longer be provenReinstall/reconfigure and approve the current declaration

The operation-level error requirements_unmet means one or more of these blockers exists. It does not, by itself, identify a lease, signing, or App Secret failure.

A common new-Feishu failure has all of these facts:

  • installation is disabled;
  • latest Enable operation failed with requirements_unmet;
  • there is no ready plugin_connection;
  • there is no current plugin_permission;
  • the device enrollment and lease are healthy.

In that case, repeating device enrollment, rotating server keys, or clicking Enable again cannot help. Complete the local profile and permission steps.

Device Connection Failures

Diagnose the device transport separately from lifecycle readiness:

LayerErrorWhat to inspect
Leaselease_failedSigned-in owner, enrollment state, generation, challenge and renewal
Trust resourcetrust_config_invalidBuild channel, WSS origin/path, public-key validity/revocation
Server signerserver_signing_unavailableTyped channel signing configuration and active key id
Transporttransport_unavailableWSS reachability and Redis binding to the current API Pod
Authorityauthority_rejectedOwner/device/ticket/generation and pinned revisions/digests
Protocolprotocol_rejectedDesktop/Runner/service protocol version and strict frame shape

Do not collapse these into “lease problem.” The recovery action is different for every layer.

Database Diagnostics

Use read-only queries and never print secrets, ciphertext, nonces, raw provider payloads, tickets, or local paths.

List the current 23 tables:

SELECT tablename
FROM pg_catalog.pg_tables
WHERE schemaname = 'public'
  AND (tablename = 'plugin' OR tablename LIKE 'plugin\_%' ESCAPE '\')
ORDER BY tablename;

Trace an installation and its latest operations:

SELECT id, plugin_key, state, revision, release_id
FROM plugin_installation
WHERE user_id = '<user-id>'
ORDER BY created_at DESC;

SELECT id, kind, state, error_code, attempt, created_at, completed_at
FROM plugin_installation_operation
WHERE installation_id = '<installation-id>'
ORDER BY created_at DESC;

Inspect non-secret connection and permission authority:

SELECT id, provider_key,
       resource_scope_json->>'profile_label' AS profile_label,
       device_id, state, credential_id, credential_version, revision
FROM plugin_connection
WHERE installation_id = '<installation-id>';

SELECT id, capability_id, revision, policy_digest, supersedes_id
FROM plugin_permission
WHERE installation_id = '<installation-id>'
ORDER BY created_at DESC;

Inspect liveness without exposing key material:

SELECT id, state, lease_generation, lease_expires_at,
       app_channel, app_version, catalog_digest
FROM plugin_device_enrollment
WHERE user_id = '<user-id>'
ORDER BY updated_at DESC;

Recommended order for an Enable failure:

  1. latest operation error code;
  2. health blockers;
  3. ready connection;
  4. current permission coverage;
  5. bound device lease and generation;
  6. only then local runtime/package/provider health.

Adding a Plugin

  1. Declare plugin identity, immutable release, capability, and platform runtime in the single catalog/manifest source. Do not duplicate deployment or presentation facts in executable code.
  2. Reuse the closed capability schema. A new protocol concept must update validation, database contracts, API projection, Runner, and tests together.
  3. Choose one credential owner: device-local, server OAuth, or no credential. Never mix ownership modes.
  4. Use package-relative provider entrypoints with closed argv, working-directory, transport, and verifier schemas. Do not construct shell commands.
  5. Declare minimum permissions. External writes must use effect intent, approval, receipt, and reconciliation.
  6. Use a local long connection or the controlled webhook relay for inbound events. Do not introduce cloud execution fallback.
  7. Test install, setup, permission, offline device, generation fence, revoked release, protocol drift, and retry/ambiguity behavior.
  8. Give Web, Desktop, and mobile users equivalent state and recovery information.
  9. Update both English and Chinese Rspress pages and provider-specific documentation.
  10. Validate installation through a real Desktop/Runner provider call.

Source Map

ConcernSource
Builtin catalog and authority digestservice/pkg/core/plugin/builtin/
Extension schemaservice/pkg/core/plugin/packagevalidator/schemas/plugin-extension.schema.json
Lifecycle and readinessservice/pkg/core/plugin/lifecycle/, service/pkg/repo/plugin/lifecycle/
Current account authorityservice/pkg/repo/plugin/currentauthority/
Connections, permissions, secretsservice/pkg/repo/plugin/profile/, service/pkg/repo/plugin/permission/, service/pkg/repo/plugin/secret/
Devices and actionsservice/pkg/core/plugin/device/, service/pkg/repo/plugin/device/
Device bridge and provider RPCservice/pkg/core/plugin/devicebridge/, service/pkg/core/plugin/localprovider/
Effectsservice/pkg/core/plugin/effect/, service/pkg/repo/plugin/effect/
Runtime sessions and eventsservice/pkg/core/plugin/runtime/, service/pkg/repo/plugin/runtime/
HTTP/WSS viewsservice/pkg/web/views/plugins/, service/pkg/web/views/plugindevices/, service/pkg/web/views/runnerws/
Web plugin UIfrontend/web/src/features/plugins/, frontend/shared/src/service/pluginService.ts
Runner executionrunner/crates/scilaxy-runner/src/plugins/
Electron credentialsdesktop/electron/src/plugins/
Dev authoritydesktop/scripts/provision-dev-plugin-runner.mjs
Current authority cutoverservice/migrations/00093_remove_session_plugin_authority.sql