Plugin

Custom Plugin

Build, publish, and configure installable Downcity Plugins

Custom Plugin

One CLI entry can export multiple Plugin constructors. Each Plugin owns its static manifest and optional Resource Resolver. The CLI installs, validates, indexes, and instantiates them uniformly.

Artifact structure

example-connectors/
├── downcity.plugin.json
└── dist/
    └── index.js

The entry exports only an array:

export const plugins = [GithubPlugin, LinearPlugin];

There is no Plugin Project or per-Plugin Factory.

Plugin constructor

import type { JsonObject } from "@downcity/agent";

export class GithubPlugin {
  static readonly manifest = {
    name: "github",
    version: "1.0.0",
    title: "GitHub",
    description: "GitHub integration",
    config: {
      schema: GITHUB_CONFIG_JSON_SCHEMA,
      defaults: {
        api_url: "https://api.github.com",
      },
    },
    resources: {
      schema: GITHUB_RESOURCE_JSON_SCHEMA,
    },
  };

  static async resolve_resource({ resource }: { resource: JsonObject }) {
    const user = await get_github_user(String(resource.token));
    return { name: user.name, login: user.login };
  }

  readonly name = "github";
  readonly actions = {};

  constructor({ config, resources }: {
    config: JsonObject;
    resources: JsonObject[];
  }) {
    // The CLI has already validated config and resolved Resources.
  }
}

The CLI uniformly executes:

new GithubPlugin({ config, resources });

The instance name must match static manifest.name.

Config and Resource Schema

Schemas use JSON Schema 2020-12 and live directly in the Plugin static manifest. Zod can be the source definition:

import { z } from "zod";

const config_schema = z.object({
  api_url: z.url(),
}).strict();

export const GITHUB_CONFIG_JSON_SCHEMA = z.toJSONSchema(config_schema, {
  target: "draft-2020-12",
});

A Resource Schema describes a complete Item and requires id, type, and name. City writes id; users provide ordinary and writeOnly fields; static resolve_resource returns the remaining readOnly fields.

Every Plugin must provide a non-empty description. City displays it consistently in the plain Plugin list, interactive list, and JSON Catalog.

The Resolver runs only while creating, editing, or refreshing a Resource. Agent startup reads the saved complete Item and never performs an implicit network refresh.

Static installation manifest

Installation cannot execute a third-party entry. The build must therefore generate and commit downcity.plugin.json from the Plugin static manifests:

{
  "manifest_version": 3,
  "entry": "dist/index.js",
  "plugins": [
    {
      "name": "github",
      "version": "1.0.0",
      "title": "GitHub",
      "config": {
        "schema": {
          "type": "object",
          "properties": {
            "api_url": { "type": "string", "format": "uri" }
          },
          "additionalProperties": false
        },
        "defaults": {
          "api_url": "https://api.github.com"
        }
      }
    },
    {
      "name": "linear",
      "version": "1.0.0",
      "title": "Linear",
      "description": "Linear integration"
    }
  ]
}

At runtime, the CLI verifies that every constructor static manifest in plugins[] matches this installed snapshot.

Actions do not appear in the static manifest. The Plugin instance actions collection is the only source of runtime Actions, avoiding a duplicated name list that can drift.

Install and configure

city plugin install ./example-connectors
city plugin install github:acme/example-connectors#main

city plugin resource create github --interactive
city plugin config github my-agent --interactive
city plugin enable github my-agent

city plugin update github
city plugin uninstall github

If one entry exports both github and linear, updating either updates the complete shared entry. Uninstalling either removes every Plugin from that entry. The CLI refuses removal while any sibling Plugin still has a Binding or Resource.

Trust boundary

Installation reads only the static manifest and artifact files. It never runs npm install, build scripts, lifecycle scripts, or the Plugin entry. City records the resolved Git commit and a SHA-256 digest of the complete artifact.

The Plugin entry executes while creating or refreshing Resources and when an Agent starts, so install only trusted sources.

Continue with: