Plugin Runtime and Security

Plugin security protects a local execution boundary, not merely a marketplace page. A Runner can hold third-party credentials, read user-authorized files, start binaries, and perform externally visible writes. Every remote instruction must therefore identify the exact user, device generation, immutable release, connection, permission, and payload.

Execution Boundary

ComponentResponsibilitiesMust not do
Web/mobile rendererShow catalog, profile names, health, setup, and approvalsRead operational secrets, executable paths, or start providers
APICatalog, installation, non-secret connection metadata, permissions, device public keys, challenges/tickets, signed RPC, controlled GitHub ingressStore device-local secrets, execute plugins, or choose a fallback device
WorkerRun lifecycle state machines, resolve current authority, prepare tools, dispatch through the device bridgeDownload/start providers or create an offline plugin queue
Redis device bridgeRoute bounded short-lived requests to the API Pod that owns an exact WSS connectionPersist credentials or wait for an offline device
Electron MainIsolated credential prompts, OS-backed encryption, one-shot local bootstrapExpose secrets to renderer, API, logs, or public DTOs
RunnerVerify authority, manage package cache, supervise providers, journal inbound eventsAccept generic remote shell commands or reuse another channel's authority
Local providerSpeak provider API, MCP, Channel, or Agent Runtime protocolsAccess the SciLaxy database or become a server sidecar
sequenceDiagram
  participant Worker
  participant Authority as Current authority
  participant Bridge as Redis device bridge
  participant API as API / WSS
  participant Runner
  participant Desktop as Electron Main
  participant Provider

  Worker->>Authority: Resolve release, connection, permission, device
  Worker->>Bridge: Bounded provider request
  Bridge->>API: Route to exact connection
  API->>API: Sign authority and body digest
  API->>Runner: plugin_rpc
  Runner->>Runner: Verify signature, revisions, generation, expiry
  Runner->>Desktop: Request one-shot local bootstrap
  Desktop-->>Runner: Decrypted local credential
  Runner->>Provider: tools/list or tools/call
  Provider-->>Runner: Closed result or error
  Runner-->>Worker: Request-bound response

If the selected device is offline, fenced, missing its verified runtime, or cannot durably persist an inbound event, the plugin is unavailable. The system does not select another device, execute on the server, or retain work for later execution.

Device Trust Chain

Registration, lease, tickets, and WSS solve different problems:

MechanismLifetimeEvidence
Device enrollmentDurable until fenced or revokedThe user's account trusts the holder of this device Ed25519 private key; the server stores only the public key
Lease challenge and renewalShort and continuously renewedThe same key holder is online with the expected generation, channel, version, and catalog
User-gesture ticketVery short, single purposeA sensitive device action follows a recent explicit user gesture
WSS ticketVery short, single connectionOne identified device may open the Runner control channel
Signed RPC envelopeOne requestOne payload is authorized for one exact authority set and expiry window

Reconnecting a valid Desktop must reuse its enrollment. Re-registering on every click creates duplicate identities and does not repair a lease or readiness failure. Revocation increments the generation, fencing previous WSS bindings, actions, runtime sessions, and calls.

Channel Trust Resources

Each Desktop release channel consumes one channel-specific trust resource. Startup validates all of the following:

  • build channel equals trust-resource channel;
  • plugin WSS origin is the same as the current backend origin;
  • the path is exactly /scilaxy/ws/v1/runner/device;
  • at least one Ed25519 public key is valid for the current time and not revoked;
  • the server's signing key id resolves to that channel's trusted key.

Dev, Beta/Test, RC/UAT, and Stable/Prod never share a runtime signing identity. Cross-channel fallback would allow a weaker environment to control another channel's local Runner. Origin and exact-path checks prevent proxy drift from redirecting the local execution channel to an unrelated WebSocket endpoint.

These checks are appropriate at the execution boundary. Product surfaces must still distinguish lease_failed, trust_config_invalid, server_signing_unavailable, transport failures, protocol failures, and ordinary readiness blockers. Strong security does not justify an opaque generic error.

Runtime Packages

Optional first-party provider runtimes are not bundled into the Desktop installer and never execute in API or Worker containers.

The Desktop build carries a build-bound plugin-runtime-manifest.json. On first use, the selected Runner:

  1. resolves the release's platform archive;
  2. downloads it into the current channel's private cache;
  3. verifies the manifest, archive digest, and every declared member;
  4. enforces each member's declared executable role;
  5. atomically publishes the verified cache entry;
  6. re-verifies cached evidence before later execution.

Download failure, an unsupported platform, digest mismatch, or missing cache makes the capability unavailable. There is no server provider-host fallback.

Plugin code may contain protocol constants, schema versions, canonical API paths, closed enums, and safety bounds. Product catalog entries belong in builtin/catalog.json; environment origins, channel selection, signing keys, and deployment secrets come from typed build or runtime configuration. Provider protocol endpoints belong only in their provider adapter.

Credential Boundary

Device-local setup uses a plugin_device_action containing only a provider key, profile label, target device/generation, and request digest. The action expires if it is not claimed and completed.

For Feishu:

  1. the user chooses a profile label and online Desktop;
  2. Desktop claims the action using a short user-gesture ticket;
  3. Electron Main opens an isolated prompt for cn or global, App ID, and App Secret;
  4. safeStorage encrypts the record into the current channel's private plugin directory;
  5. Desktop signs completion; the server atomically completes the action and creates a ready connection;
  6. Runner obtains a one-shot local bootstrap and starts the provider long connection.

The API, Worker, renderer, and public DTOs receive only a profile label, opaque profile id, device id, connection/credential revisions, and irreversible digests. For Feishu and GitHub device-local connections, plugin_connection.credential_id remains null and no operational secret is stored in plugin_secret.

Signed Provider Calls

The inner plugin-provider/1 protocol currently allows closed tools.list and tools.call requests. The outer signed envelope binds:

  • signing key id, owner, device, channel, and app version;
  • device generation, connection id, nonce, and request id;
  • installation, release, capability, and authority digests;
  • connection and permission revisions plus credential version;
  • issue time, expiry, and complete body digest.

The Runner rejects unknown fields, duplicate JSON keys, expired requests, body mismatch, revision drift, authority drift, and device-generation drift before provider I/O.

External Effects

Potential external writes are represented by plugin_effect_intent. The immutable envelope records exactly what was approved: action, arguments, destination, release/tool/config evidence, connection/permission revisions, device generation, runtime/workspace authority, and provider idempotency key.

The state distinguishes safe failure from ambiguity:

  • failed_before_dispatch: no provider boundary was crossed; retry can be safe.
  • uncertain: the provider may have accepted the write; do not automatically replay.
  • reconciled_succeeded or reconciled_failed: provider evidence resolved the ambiguity.

External writes cannot be configured as permanently allowed. They require a prompt or are denied.

Inbound and Webhooks

Feishu uses its official long connection from the local provider. Runner journals the event, sends it over the authenticated device channel, and waits for server durable acceptance. The server deduplicates, validates current route authority, creates the inbound event/outbox and Turn transactionally, then acknowledges the local digest. An offline device means no server-side collection or deferred delivery.

GitHub Webhook is the controlled public-ingress exception:

  1. the endpoint is bound to exact installation/connection revisions, device generation, release, and authority digest;
  2. the server verifies X-Hub-Signature-256, event type, delivery id, size, and rate limits;
  3. it forwards only while the bound device is online;
  4. Runner durably persists the event and returns a digest-bound ACK;
  5. the server returns provider success only after that ACK;
  6. offline, fenced, timeout, or local-persistence failure returns a retryable failure without an offline work queue.

The server stores webhook verifier material because it must authenticate public ingress, but that material grants no GitHub API authority. It does not store the GitHub token or raw delivery body.

Security Invariants

  • Plugin code, operational credentials, processes, calls, and accessed local files live only on the selected Desktop/Runner.
  • Device absence is an explicit unavailable result, never a reason to fall back to another device or cloud execution.
  • Renderer, public DTOs, API/Worker logs, and audit metadata never contain device-local secrets or executable paths.
  • Release, config, tool catalog, permission, connection, device generation, or runtime drift fences old asynchronous work before provider I/O.
  • Inbound success requires durable acceptance; external-write ambiguity requires reconciliation rather than blind retry.
  • Removed Session snapshot tables, legacy provider-host execution, fallback wire formats, and cross-channel trust are not supported.