Skip to main content

Overview

This guide walks through building a complete ArgentOS plugin. Plugins can register tools, read service keys, persist configuration, and hook into the agent lifecycle — all without modifying core files.

Plugin Structure

~/.argentos/extensions/my-plugin/
├── argent.plugin.json    # Manifest (required)
└── index.ts              # Plugin entry point
Plugins are discovered from two locations:
  • Global: ~/.argentos/extensions/<plugin-id>/
  • Workspace: .argent/extensions/<plugin-id>/ (project-local)
The entry point is loaded by Jiti (TypeScript JIT), so .ts files work without a build step.

Manifest

Every plugin needs an argent.plugin.json for discovery and config validation:
{
  "id": "my-plugin",
  "name": "My Plugin",
  "description": "What this plugin does",
  "version": "1.0.0",
  "configSchema": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "apiKey": {
        "type": "string",
        "description": "API key (prefer service-keys.json)"
      },
      "userId": {
        "type": "number",
        "description": "Linked user ID"
      }
    }
  },
  "uiHints": {
    "apiKey": { "sensitive": true, "label": "API Key" }
  }
}
FieldRequiredDescription
idYesUnique plugin identifier
configSchemaYesJSON Schema for plugin config (even if empty)
nameNoDisplay name
descriptionNoShort summary
versionNoSemver version
uiHintsNoUI rendering hints (sensitive flags, labels, placeholders)
kindNoPlugin kind (e.g., "memory")
channelsNoChannel IDs this plugin registers
providersNoProvider IDs this plugin registers
skillsNoSkill directories to load

Entry Point

Export a default function that receives the registration API:
export default function register(api: any) {
  api.registerTool({ /* ... */ });
  api.on("before_agent_start", async (ctx: any) => { /* ... */ });
}

No Core Imports

Plugins in ~/.argentos/extensions/ don’t have access to core node_modules. Use plain JSON Schema objects instead of @sinclair/typebox.
// Don't: import { Type } from "@sinclair/typebox"
// Do: plain JSON Schema
const schema = {
  type: "object" as const,
  properties: {
    query: { type: "string" as const, description: "Search query" },
    limit: { type: "number" as const, description: "Max results" },
  },
  required: ["query"] as const,
};

Registering Tools

api.registerTool({
  name: "my_tickets",
  description: "List support tickets",
  parameters: {
    type: "object",
    properties: {
      status: {
        type: "string",
        enum: ["open", "pending", "resolved"],
        description: "Filter by status",
      },
    },
  },
  async execute(_id: string, params: any) {
    const data = await fetchTickets(params);
    return {
      content: [{ type: "text", text: JSON.stringify(data, null, 2) }],
    };
  },
});
For tools that require credentials or have side effects, mark them as optional:
api.registerTool(
  { name: "dangerous_tool", /* ... */ },
  { optional: true },
);

Service Keys

ArgentOS has a centralized key store at ~/.argentos/service-keys.json, managed through the dashboard (Settings > API Keys). Plugins should read from this store rather than requiring separate config.

Reading Service Keys

function resolveApiKey(variableName: string): string | undefined {
  try {
    const fs = require("node:fs");
    const path = require("node:path");
    const keysPath = path.join(
      process.env.HOME ?? "/tmp",
      ".argentos",
      "service-keys.json",
    );
    const raw = fs.readFileSync(keysPath, "utf-8");
    const store = JSON.parse(raw);
    const entry = (store.keys ?? []).find(
      (k: any) => k.variable === variableName && k.enabled !== false,
    );
    if (entry?.value) return entry.value;
  } catch {}
  return process.env[variableName]; // Fallback for CI/servers
}

How It Works

1

User adds the key

Through Dashboard > Settings > API Keys.
2

Dashboard writes to store

Dashboard writes to ~/.argentos/service-keys.json.
3

Plugin reads at runtime

Plugin reads the key at runtime via the pattern above.
4

Fallback for CI

Falls back to process.env for CI/server deployments.

Service Keys Format

{
  "keys": [
    {
      "variable": "MY_API_KEY",
      "value": "sk-...",
      "enabled": true,
      "label": "My Service",
      "category": "other"
    }
  ]
}

Config Persistence

Plugins can persist configuration to ~/.argentos/argent.json under plugins.entries.<id>.config:
function updatePluginConfig(
  pluginId: string,
  key: string,
  value: any,
): boolean {
  try {
    const fs = require("node:fs");
    const path = require("node:path");
    const configPath = path.join(
      process.env.HOME ?? "/tmp",
      ".argentos",
      "argent.json",
    );
    const raw = fs.readFileSync(configPath, "utf-8");
    const config = JSON.parse(raw);
    if (!config.plugins) config.plugins = {};
    if (!config.plugins.entries) config.plugins.entries = {};
    if (!config.plugins.entries[pluginId])
      config.plugins.entries[pluginId] = { enabled: true };
    if (!config.plugins.entries[pluginId].config)
      config.plugins.entries[pluginId].config = {};
    config.plugins.entries[pluginId].config[key] = value;
    fs.writeFileSync(
      configPath,
      JSON.stringify(config, null, 2) + "\n",
      "utf-8",
    );
    return true;
  } catch {
    return false;
  }
}
This enables self-service setup flows where the agent discovers and saves config automatically.

Config in argent.json

{
  "plugins": {
    "allow": ["my-plugin"],
    "entries": {
      "my-plugin": {
        "enabled": true,
        "config": {
          "userId": 12345
        }
      }
    }
  }
}

Plugin Allowlist

If plugins.allow[] has any entries, only plugins in that list are enabled. Non-bundled plugins default to enabled when the allowlist is empty.If your plugin isn’t loading, check whether an allowlist exists and add your plugin ID.

Lifecycle Hooks

before_agent_start

Fires before each agent run. Inject context or nudge the agent:
api.on("before_agent_start", async (ctx: any) => {
  const apiKey = resolveApiKey("MY_API_KEY");
  if (!apiKey) return;

  const summary = await fetchSummary(apiKey);
  ctx.systemPromptSuffix = `
## My Integration
${summary}
  `.trim();
});
Use systemPromptSuffix for:
  • Open ticket counts or active alerts
  • Nudging the agent when setup is incomplete
  • Injecting operator-specific context

Tips

Jiti loads TypeScript directly.
Don’t create mega-tools — split tickets, devices, and alerts into separate tools.
Return a helpful message if the API key is missing, don’t throw.
Prevent hung API calls from blocking the agent.
Users manage keys through the dashboard UI.
Let the agent discover configuration instead of requiring manual entry.