Skip to main content

Prerequisites

  • Python 3.11+
  • Target vendor API docs reviewed
  • ArgentOS repo cloned (tools/aos/ directory available)

Directory Structure

Every connector lives at tools/aos/aos-{name}/:
tools/aos/aos-{name}/
├── connector.json                    <- Authoritative metadata
├── README.md                         <- Operator docs
└── agent-harness/
    ├── permissions.json              <- Mode-gate map
    ├── pyproject.toml                <- Python config + CLI entry point
    ├── cli_aos/
    │   └── {name}/
    │       ├── __init__.py           <- Version marker
    │       ├── constants.py          <- Env var names, API base URLs
    │       ├── errors.py             <- CliError dataclass
    │       ├── output.py             <- JSON/text helpers
    │       ├── config.py             <- Auth resolution from env vars
    │       ├── client.py             <- HTTP client (actual API calls)
    │       ├── runtime.py            <- Command functions + health
    │       └── cli.py                <- Click CLI wiring
    └── tests/
        ├── conftest.py               <- sys.path setup
        └── test_cli.py               <- FakeClient mocking

Step 1: Copy the Reference Template

cp -r tools/aos/templates/python-click-tool tools/aos/aos-{name}
cd tools/aos/aos-{name}
Or copy from an existing connector as a reference:
cp -r tools/aos/aos-twilio tools/aos/aos-{name}
Rename all internal references from the source connector to your new name.

Step 2: Write connector.json

This is the authoritative manifest. It tells ArgentOS what your connector does, what commands it supports, what auth it needs, and how to render it in the dashboard.
{
  "tool": "aos-{name}",
  "backend": "{vendor}-api",
  "manifest_schema_version": "1.0.0",
  "connector": {
    "label": "My Vendor",
    "category": "category-name",
    "categories": ["primary-category", "secondary"],
    "resources": ["resource1", "resource2"]
  },
  "auth": {
    "kind": "service-key",
    "required": true,
    "service_keys": ["VENDOR_API_KEY"],
    "interactive_setup": [
      "Get an API key from vendor.com/settings",
      "Add VENDOR_API_KEY in API Keys settings"
    ]
  },
  "commands": [
    {
      "id": "items.list",
      "summary": "List items",
      "required_mode": "readonly",
      "supports_json": true,
      "resource": "items",
      "action_class": "read"
    },
    {
      "id": "items.create",
      "summary": "Create a new item",
      "required_mode": "write",
      "supports_json": true,
      "resource": "items",
      "action_class": "write"
    }
  ]
}

Step 3: Write permissions.json

Map every CLI command to a permission tier:
{
  "commands": {
    "health": "readonly",
    "config.show": "readonly",
    "capabilities": "readonly",
    "items.list": "readonly",
    "items.get": "readonly",
    "items.create": "write",
    "items.update": "write",
    "items.delete": "admin"
  }
}

Step 4: Implement client.py

This is the core — the HTTP client that calls the actual vendor API.
import json
import urllib.request
import urllib.error

class Client:
    def __init__(self, api_key: str, base_url: str = "https://api.vendor.com/v1"):
        self.api_key = api_key
        self.base_url = base_url

    def _request(self, method: str, path: str, data=None):
        url = f"{self.base_url}{path}"
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }
        body = json.dumps(data).encode() if data else None
        req = urllib.request.Request(url, data=body, headers=headers, method=method)
        try:
            with urllib.request.urlopen(req) as resp:
                return json.loads(resp.read().decode())
        except urllib.error.HTTPError as e:
            raise RuntimeError(f"API error {e.code}: {e.read().decode()}")

    def list_items(self, limit=30):
        return self._request("GET", f"/items?limit={limit}")

    def create_item(self, title: str, description: str = ""):
        return self._request("POST", "/items", {"title": title, "description": description})
Guidelines:
  • Use stdlib urllib (no external HTTP dependencies)
  • One method per command
  • Return structured dicts, not raw responses
  • Auth resolved from constructor params (injected by config.py)

Step 5: Implement runtime.py

Command result functions and the three required contracts:
from .client import Client
from .config import resolve_config
from .output import success, error

def health():
    cfg = resolve_config()
    if not cfg.get("api_key"):
        return {"status": "needs_setup", "checks": {"auth": False}, "next_steps": ["Set VENDOR_API_KEY"]}
    try:
        client = Client(cfg["api_key"])
        client.list_items(limit=1)
        return {"status": "healthy", "checks": {"auth": True, "api": True}}
    except Exception as e:
        return {"status": "degraded", "checks": {"auth": True, "api": False}, "detail": str(e)}

def items_list(mode, limit=30):
    cfg = resolve_config()
    client = Client(cfg["api_key"])
    return success("items.list", client.list_items(limit))

Step 6: Implement cli.py

Wire commands to runtime functions using Click:
import click
from .runtime import health, items_list
from .output import format_output

@click.group()
@click.option("--json", "use_json", is_flag=True)
@click.option("--mode", default="readonly")
@click.option("--verbose", is_flag=True)
@click.pass_context
def cli(ctx, use_json, mode, verbose):
    ctx.ensure_object(dict)
    ctx.obj["json"] = use_json
    ctx.obj["mode"] = mode

@cli.command()
@click.pass_context
def health_cmd(ctx):
    """Report connector health."""
    click.echo(format_output(health(), ctx.obj["json"]))

# ... more commands
Entry point in pyproject.toml:
[project.scripts]
aos-{name} = "cli_aos.{name}.cli:cli"

Step 7: Write Tests

cd agent-harness
python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
python -m pytest tests/ -v
Required test coverage:
  • Commands match connector.json manifest
  • Health: missing creds returns needs_setup, valid creds returns healthy
  • Read commands return expected data shapes (use FakeClient)
  • Write commands respect mode gating (calling write in readonly mode fails)

Step 8: Verify End-to-End

# Check capabilities
aos-{name} --json capabilities | python -m json.tool

# Check health
aos-{name} --json health | python -m json.tool

# Run tests
python -m pytest tests/ -v

Certification Gates

Before a connector is production-ready:
GateRequirement
ContractValid connector.json, working capabilities, health, config show
SafetyWrite actions classified, destructive actions gated at admin mode
AuditTool calls preserve evidence, failures return understandable errors
DocsREADME exists with setup instructions and known limitations
TestsAll required test categories pass

Risk Tiers

TierAccess LevelExamplesReview Required
1: Read-heavyQueries onlySearch tickets, list contactsMinimal
2: Bounded writeScoped mutationsReply to message, create eventStandard
3: Business mutationBusiness state changesCRM stage change, invoice creationThorough
4: Destructive/adminIrreversible operationsDelete records, revoke accessExplicit approval