Skip to main content

Plugin SDK Reference

Protocol Specification

Arcan plugins communicate with the core over JSON-over-stdin/stdout. The core spawns the plugin as a child process, sends one JSON object per line on stdin, and reads one JSON object per line from stdout.

Request Format

{"method": "describe", "params": null}
{"method": "ping", "params": null}
{"method": "generate", "params": {"role": "readonly", "ttl": "1h"}}
{"method": "validate", "params": {"username": "arcan_a1b2c3d4"}}
FieldTypeDescription
methodstringOne of: describe, ping, generate, validate
paramsjson.RawMessageMethod-specific parameters (null for describe/ping)

Response Format

Success:

{"data": {"name": "postgres", "version": "0.1.0"}}

Error:

{"error": "unknown role \"superadmin\" — use readonly, readwrite, or admin"}

Only one of data or error is present in a response.

Methods

MethodPurposeRequires SecretEngine
describeReturn plugin identity and capabilitiesNo
pingVerify the plugin is healthyNo
generateCreate new credentialsYes
validateValidate or revoke existing credentialsYes

The Engine Interface

Every plugin implements either Engine (basic) or SecretEngine (credentials).

// Engine is the interface every plugin must implement.
type Engine interface {
Describe() Descriptor
Ping() error
}

// SecretEngine extends Engine with secret generation and validation.
type SecretEngine interface {
Engine
Generate(params json.RawMessage) (*SecretResult, error)
Validate(params json.RawMessage) (*ValidationResult, error)
}

Descriptor

type Descriptor struct {
Name string `json:"name"`
Version string `json:"version"`
DisplayName string `json:"display_name"`
Description string `json:"description"`
Capabilities []string `json:"capabilities"`
}

SecretResult / ValidationResult

type SecretResult struct {
Data map[string]any `json:"data"`
}

type ValidationResult struct {
Valid bool `json:"valid"`
Message string `json:"message,omitempty"`
}

Plugin Skeleton

Minimal compilable plugin (30 lines):

package main

import (
"encoding/json"

"getarcan.dev/arcan/sdk"
)

type MyEngine struct{}

func (e *MyEngine) Describe() sdk.Descriptor {
return sdk.Descriptor{
Name: "my-engine",
Version: "0.1.0",
DisplayName: "My Engine",
Description: "Description of what this engine does",
Capabilities: []string{"dynamic_credentials"},
}
}

func (e *MyEngine) Ping() error {
return nil
}

func (e *MyEngine) Generate(params json.RawMessage) (*sdk.SecretResult, error) {
return &sdk.SecretResult{
Data: map[string]any{"secret": "value"},
}, nil
}

func (e *MyEngine) Validate(params json.RawMessage) (*sdk.ValidationResult, error) {
return &sdk.ValidationResult{Valid: true, Message: "ok"}, nil
}

func main() {
sdk.Serve(&MyEngine{})
}

sdk.Serve starts a bufio.Scanner loop on stdin. Each line is dispatched to the appropriate method. The loop runs until stdin closes.

Building Your Plugin

Directory Structure

my-engine/
go.mod
main.go
README.md
Makefile

go.mod

module my-engine

go 1.22

require getarcan.dev/arcan/sdk v0.1.0

Build Commands

Native binary:

go build -o arcan-engine-my-engine .

WebAssembly (WASM):

GOOS=wasip1 GOARCH=wasm go build -o arcan-engine-my-engine.wasm .

Scaffolder

arcan plugin init my-engine

Generates a working plugin directory with go.mod, main.go, Makefile, and README.md.

Signing and Installation

Generate a Signing Key Pair

arcan plugin keygen

Sign the Plugin Binary

arcan plugin sign arcan-engine-my-engine

Creates arcan-engine-my-engine.sig alongside the binary.

Install

Copy the binary and signature to the plugins directory:

cp arcan-engine-my-engine ~/.arcan/data/plugins/
cp arcan-engine-my-engine.sig ~/.arcan/data/plugins/

What Happens on Server Startup

  1. Discovery: The server scans ~/.arcan/data/plugins/ for binaries.
  2. Signature verification: Each binary's .sig file is verified against the registered Ed25519 public key. Unsigned plugins are rejected.
  3. Describe: The server spawns each verified plugin, sends {"method":"describe"}, and registers its capabilities.

Testing Your Plugin

Manual Testing (stdin/stdout)

# Describe
echo '{"method":"describe"}' | ./arcan-engine-my-engine

# Ping
echo '{"method":"ping"}' | ./arcan-engine-my-engine

# Generate
echo '{"method":"generate","params":{"role":"readonly","ttl":"1h"}}' | ./arcan-engine-my-engine

# Validate (revoke)
echo '{"method":"validate","params":{"username":"arcan_a1b2c3d4"}}' | ./arcan-engine-my-engine

Official Plugins Reference

PluginTypeCapabilitiesRoles
PostgreSQLDatabasedynamic_credentials, root_rotationreadonly, readwrite, admin
MySQLDatabasedynamic_credentials, root_rotationreadonly, readwrite, admin
MongoDBDatabasedynamic_credentialsread, readWrite, dbAdmin
RedisDatabasedynamic_credentialsreadonly, readwrite, admin
AWS IAMCloudengine:dynamic_credentialsreadonly, poweruser, admin, custom
AzureCloudengine:dynamic_credentialsreader, contributor, owner
GCPCloudengine:dynamic_credentialsviewer, editor, owner, custom
SSHCryptosecret_generation, secret_validationN/A (principals-based)
KubernetesInfrastructuresecret_generation, secret_validationviewer, editor, admin
RabbitMQInfrastructuresecret_generation, secret_validationconsumer, producer, admin
PKICryptosecret_generation, secret_validationserver, client, both

Configuration Patterns

Database plugins (PostgreSQL, MySQL, MongoDB, Redis): No configuration at startup. The core manages root credentials. The plugin returns SQL/commands for the core to execute.

Cloud plugins (AWS IAM, Azure, GCP): Load credentials from the ARCAN_ENGINE_CONFIG environment variable (JSON object).

Infrastructure plugins (Kubernetes, RabbitMQ): Use individual environment variables (ARCAN_KUBE_*, ARCAN_RABBITMQ_*).

Crypto plugins (SSH, PKI): Load CA keys from environment variables with optional fallback to ephemeral keys for development.

Environment Variables

VariableUsed byFormat
ARCAN_ENGINE_CONFIGaws-iam, azure, gcpJSON object
ARCAN_SSH_CA_PRIVATE_KEYsshPEM-encoded Ed25519/RSA private key
ARCAN_PKI_CA_CERTpkiPEM-encoded X.509 certificate
ARCAN_PKI_CA_KEYpkiPEM-encoded ECDSA private key
ARCAN_PKI_ORGANIZATIONpkiString (default: "Arcan PKI")
ARCAN_KUBE_API_SERVERkubernetesURL
ARCAN_KUBE_TOKENkubernetesBearer token string
ARCAN_KUBE_NAMESPACEkubernetesString (default: "default")
ARCAN_KUBE_SKIP_TLSkubernetes"true" to skip TLS verification
ARCAN_RABBITMQ_URLrabbitmqManagement API URL
ARCAN_RABBITMQ_USERrabbitmqAdmin username
ARCAN_RABBITMQ_PASSrabbitmqAdmin password
ARCAN_RABBITMQ_VHOSTrabbitmqDefault vhost (default: "/")

Generate Input/Output Summary

PluginInputOutput
PostgreSQL{role, ttl}{username, password, expires_at, sql_statements}
MySQL{role, ttl, database}{username, password, expires_at, database, sql_statements}
MongoDB{role, ttl, database, auth_database}{username, password, expires_at, database, auth_database, mongo_command}
Redis{role, ttl}{username, password, expires_at, redis_command}
AWS IAM{role, ttl, policy_arn}{access_key_id, secret_access_key, username, account_id, region, policy_arn}
Azure{role, ttl}{client_id, client_secret, tenant_id, subscription_id, app_object_id, sp_object_id, role, display_name}
GCP{role, ttl, iam_role}{email, key_json, project_id, iam_role, unique_id}
SSH{public_key, principals, ttl, key_id}{signed_certificate, serial, key_id, principals, valid_after, valid_until, ca_public_key}
Kubernetes{namespace, role, ttl}{token, namespace, service_account, role_binding, cluster_role, expires_at}
RabbitMQ{role, vhost}{username, password, vhost, management_url, amqp_url, permissions}
PKI{common_name, san, role, ttl}{certificate, private_key, ca_certificate, common_name, san, serial, not_before, expires_at}