> ## Documentation Index
> Fetch the complete documentation index at: https://docs.argentos.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Building Plugins

> End-to-end guide to building ArgentOS plugins — tools, hooks, service keys, and config persistence.

## 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)

<Tip>
  The entry point is loaded by Jiti (TypeScript JIT), so `.ts` files work without a build step.
</Tip>

## Manifest

Every plugin needs an `argent.plugin.json` for discovery and config validation:

```json theme={null}
{
  "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" }
  }
}
```

| Field          | Required | Description                                                |
| -------------- | -------- | ---------------------------------------------------------- |
| `id`           | Yes      | Unique plugin identifier                                   |
| `configSchema` | Yes      | JSON Schema for plugin config (even if empty)              |
| `name`         | No       | Display name                                               |
| `description`  | No       | Short summary                                              |
| `version`      | No       | Semver version                                             |
| `uiHints`      | No       | UI rendering hints (sensitive flags, labels, placeholders) |
| `kind`         | No       | Plugin kind (e.g., `"memory"`)                             |
| `channels`     | No       | Channel IDs this plugin registers                          |
| `providers`    | No       | Provider IDs this plugin registers                         |
| `skills`       | No       | Skill directories to load                                  |

## Entry Point

Export a default function that receives the registration API:

```ts theme={null}
export default function register(api: any) {
  api.registerTool({ /* ... */ });
  api.on("before_agent_start", async (ctx: any) => { /* ... */ });
}
```

### No Core Imports

<Warning>
  Plugins in `~/.argentos/extensions/` don't have access to core `node_modules`. Use plain JSON Schema objects instead of `@sinclair/typebox`.
</Warning>

```ts theme={null}
// 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

```ts theme={null}
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:

```ts theme={null}
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

```ts theme={null}
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

<Steps>
  <Step title="User adds the key">
    Through **Dashboard > Settings > API Keys**.
  </Step>

  <Step title="Dashboard writes to store">
    Dashboard writes to `~/.argentos/service-keys.json`.
  </Step>

  <Step title="Plugin reads at runtime">
    Plugin reads the key at runtime via the pattern above.
  </Step>

  <Step title="Fallback for CI">
    Falls back to `process.env` for CI/server deployments.
  </Step>
</Steps>

### Service Keys Format

```json theme={null}
{
  "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`:

```ts theme={null}
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

```json theme={null}
{
  "plugins": {
    "allow": ["my-plugin"],
    "entries": {
      "my-plugin": {
        "enabled": true,
        "config": {
          "userId": 12345
        }
      }
    }
  }
}
```

## Plugin Allowlist

<Info>
  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.
</Info>

## Lifecycle Hooks

### `before_agent_start`

Fires before each agent run. Inject context or nudge the agent:

```ts theme={null}
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

<AccordionGroup>
  <Accordion title="No build step needed">
    Jiti loads TypeScript directly.
  </Accordion>

  <Accordion title="One tool per concern">
    Don't create mega-tools — split tickets, devices, and alerts into separate tools.
  </Accordion>

  <Accordion title="Fail gracefully">
    Return a helpful message if the API key is missing, don't throw.
  </Accordion>

  <Accordion title="Use AbortSignal.timeout()">
    Prevent hung API calls from blocking the agent.
  </Accordion>

  <Accordion title="Prefer service keys">
    Users manage keys through the dashboard UI.
  </Accordion>

  <Accordion title="Self-service setup">
    Let the agent discover configuration instead of requiring manual entry.
  </Accordion>
</AccordionGroup>

## Related

* [Plugin System](/tools/plugins) — Plugin discovery, manifests, and management
* [Marketplace](/marketplace) — Browse and publish packages
