Service Plugins
Expose APIs for other plugins and the AI agent to call
Service plugins expose callable methods that other plugins and the AI agent can invoke. This enables cross-plugin communication and turns your extensions into tools for the built-in AI.
Overview
When you declare a services[] array in your manifest, your plugin becomes a service provider. Other plugins (and the AI agent) can call your service methods through the service broker.
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Plugin A │ │ Service Broker │ │ Plugin B │
│ (caller) │────▶│ (dispatcher) │────▶│ (service) │
│ │ │ │ │ │
│ services.call( │ │ - Permission │ │ math.add() │
│ "math", │ │ intersection │ │ math.multiply()│
│ "add", {...} │ │ - Validation │ │ │
│ ) │ │ - Routing │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘Service Definition
Manifest Declaration
{
"kunkun": {
"services": [
{
"name": "math",
"description": "Basic math operations",
"main": "dist/node/MathService.js",
"serviceMode": "node-headless",
"methods": [
{
"name": "add",
"description": "Add two numbers together",
"inputSchema": {
"type": "object",
"properties": {
"a": { "type": "number", "description": "First number" },
"b": { "type": "number", "description": "Second number" }
},
"required": ["a", "b"]
},
"outputSchema": {
"type": "object",
"properties": {
"result": { "type": "number" }
}
}
},
{
"name": "multiply",
"description": "Multiply two numbers",
"inputSchema": {
"type": "object",
"properties": {
"a": { "type": "number" },
"b": { "type": "number" }
},
"required": ["a", "b"]
},
"outputSchema": {
"type": "object",
"properties": {
"result": { "type": "number" }
}
}
}
]
}
]
}
}Service Implementation
Create a service plugin with the ServiceDefinition pattern:
// src/MathService.ts
import type { ServiceDefinition } from "@kunkunsh/api/runtime";
import * as v from "valibot";
// Define input schemas for validation
const TwoNumbersSchema = v.object({
a: v.number(),
b: v.number(),
});
// Export service definition
export default {
services: {
math: {
async add(raw: unknown) {
const { a, b } = v.parse(TwoNumbersSchema, raw);
return { result: a + b };
},
async multiply(raw: unknown) {
const { a, b } = v.parse(TwoNumbersSchema, raw);
return { result: a * b };
},
async fibonacci(raw: unknown) {
const { n } = v.parse(v.object({ n: v.number() }), raw);
let a = 0, b = 1;
for (let i = 0; i < n; i++) {
[a, b] = [b, a + b];
}
return { result: a };
},
},
// Multiple services from one plugin
text: {
async reverse(raw: unknown) {
const { text } = v.parse(v.object({ text: v.string() }), raw);
return { result: text.split("").reverse().join("") };
},
async wordCount(raw: unknown) {
const { text } = v.parse(v.object({ text: v.string() }), raw);
const words = text.trim().split(/\s+/).filter(w => w.length > 0);
return { count: words.length, words };
},
},
},
// Lifecycle hooks (optional)
onInit() {
console.log("Service initialized");
},
onDestroy() {
console.log("Service destroyed");
},
} satisfies ServiceDefinition;Build Configuration
Use the "service" mode in the build plugin:
// build.ts
await Bun.build({
entrypoints: ["./src/MathService.ts"],
outdir: "./dist/node",
target: "node",
format: "esm",
minify: true,
plugins: [kunkunCommandPlugin({ mode: "service" }) as BunPlugin],
});Consuming Services
From Another Plugin
import { services } from "@kunkunsh/api";
// Call a service method
const result = await services.call("com.example.math-plugin", "math", "add", {
a: 5,
b: 3,
});
console.log(result); // { result: 8 }With Type Safety
Define the service interface and use the typed client:
import { createServiceClient, services } from "@kunkunsh/api";
interface MathService {
add(a: number, b: number): Promise<{ result: number }>;
multiply(a: number, b: number): Promise<{ result: number }>;
}
const mathClient = createServiceClient<MathService>(
"com.example.math-plugin",
"math"
);
const { result } = await mathClient.add(5, 3);From the AI Agent
Services are automatically bridged into the AI agent's tool registry as:
plugin__<pluginId>__<serviceName>__<methodName>For example: plugin__com.example.math-plugin__math__add
When users ask the AI to perform math operations, the agent can call your service directly.
Permission Intersection
When Plugin A calls Plugin B's service, the effective permissions are the intersection of both:
- Simple permissions: Set intersection (both must allow)
- Scoped permissions: Intersect allow lists, union deny lists
This prevents privilege escalation — a low-permission plugin cannot gain access through a high-permission service.
Permission-Keyed Worker Pools
Services run in separate workers based on the caller's permissions:
serviceWorkerPool: Map<
serviceKey,
Map<permissionKey, ServiceWorkerEntry>
>Different callers with different permissions get separate worker instances.
Service Options
Timeout Control
const result = await services.call(
"com.example.plugin",
"service",
"method",
{ data: "..." },
{ timeout: 5000 } // 5 second timeout
);Default Timeout
- Default: 30 seconds
- Maximum: 10 minutes
- Configurable: Per-call via
options.timeout
Lifecycle Hooks
export default {
services: { /* ... */ },
async onInit() {
// Called when service worker starts
// Setup database connections, load caches, etc.
},
async onDestroy() {
// Called when service worker terminates
// Cleanup resources, flush data, etc.
},
} satisfies ServiceDefinition;Timeout Behavior
- Worker-side:
onDestroyhas a 5-second hard timeout - Host-side:
terminate()has a 6-second timeout
Multiple Services
A single plugin can expose multiple services:
{
"services": [
{
"name": "math",
"description": "Math operations",
"main": "dist/node/MathService.js",
"serviceMode": "node-headless",
"methods": [/* ... */]
},
{
"name": "text",
"description": "Text utilities",
"main": "dist/node/MathService.js",
"serviceMode": "node-headless",
"methods": [/* ... */]
}
]
}Both services can be implemented in the same file with separate handlers in the ServiceDefinition.
Best Practices
- Validate input: Always use Valibot or Zod schemas to validate input
- Return structured output: Match your
outputSchemadeclaration - Document methods: Use clear descriptions in the manifest
- Handle errors: Catch and return meaningful error messages
- Keep methods focused: One method = one operation
- Use lifecycle hooks: Initialize resources in
onInit, cleanup inonDestroy
Full Example
See apps/sample-worker-ext/src/MathService.ts for a complete service plugin implementation.