aamio

Add-ons

What an add-on to aamio is, the two contracts between an add-on and the core, the manifest and the process protocol, and a runnable example. For anyone who wants to write one. The core stays what pip install aamio gives: the rendezvous, and nothing more.

What an add-on is

An add-on is a package of its own, installed beside the core, that gives the Python or PHP runtime commands under aamio addon NAME and MCP tools named aamio_addon_NAME_*. Installing it is the user's choice (pip install "aamio[domain-keys]", composer require aisenseapi/aamio-domain-keys), and installed is not enabled: nothing of an add-on runs until aamio addon enable NAME names it, recorded per package in addons.json in the home. aamio addon list says what is installed, what runs here and under which contract, and aamio addon disable NAME takes one out again, broken or not. The core finds a Python add-on through the entry point group aamio.addons and a PHP add-on through the Composer package type aamio-addon, with the short name under extra.aamio-addon.name.

Two contracts

ContractWhere it runsHow the core finds itWhat it gets
1In aamio's own processA Python module with NAME, __version__, add_commands, early, command, TOOLS and dispatch; a PHP aamio-addon.php that returns an Aamio\AddonThe runtime: the key, the home, the address book. It can do what aamio can.
2In a process of its own, started by the core for each call, with the user's own file permissionsThe manifest aamio-addon.json in the Python package or at the Composer package root, which the core reads without running any of the add-on; PHP also says "contract": 2 under extra.aamio-addonThe call's arguments, a key the core resolved, the runtime's public key where a command asks for it, a directory of its own, a short list of environment variables plus what it declares, and a deadline. Never the home's key.

An add-on that says nothing is on contract 1. One written for a contract this aamio does not know is listed with the reason, cannot be enabled, and can be disabled. New add-ons should be on contract 2: a process that hangs is stopped at its deadline and the server goes on, one that crashes or answers something else is an unreachable answer with the reason, and list and help run none of its code. enable starts the process once, before the choice is recorded, to see that it answers as its manifest says. Neither contract is a sandbox. The process runs as the same user, with the user's own file permissions, and reads what the user reads; what contract 2 gives is that the core never sends the home's key, only the public key a call asks for, that nothing an add-on answers changes state in the core, and that the core knows what it offers before it runs. The environment a call gets is a short list, the path, a temporary directory, the locale, the interpreter's own settings, TLS roots and the proxy variables, which can carry credentials of their own, plus the variables the manifest names in environment: aamio addon list shows those names before the add-on is enabled, and the probe at enable does not get them. Enable what you trust, as with any package.

The manifest

The smallest one, from the example add-on hello:

{
  "aamio_addon": 2,
  "name": "hello",
  "version": "0.1.0",
  "process": {
    "python": "aamio_hello.process",
    "php": "process.php"
  },
  "deadline_seconds": 10,
  "commands": {
    "wave": {
      "help": "answer with a greeting",
      "arguments": [
        {
          "name": "--to",
          "help": "whom to greet"
        }
      ]
    }
  },
  "tools": [
    {
      "name": "aamio_addon_hello_wave",
      "description": "Answer with a greeting.",
      "inputSchema": {
        "type": "object",
        "properties": {
          "to": {
            "type": "string"
          }
        },
        "additionalProperties": false
      }
    }
  ]
}
FieldMeaning
aamio_addonThe contract: 2.
nameThe short name, 1 to 32 characters of a-z0-9_-: the command aamio addon NAME and the middle of the tool names. Equal to the entry point name (Python) and extra.aamio-addon.name (PHP). list, enable, disable and run are reserved.
versionThe add-on's version. The probe at enable must answer the same.
processHow the process starts: python names a module run as python -m MODULE with the interpreter aamio runs under; php names a file in the package run with the PHP aamio runs under. Give the one your runtime needs, or both.
deadline_secondsPer call, 1 to 120, default 30. At the deadline the process is killed and the answer is unreachable, addon_timeout.
descriptionOptional text shown by aamio addon NAME --help.
environmentOptional list of environment variable names the add-on reads, as ["AAMIO_DOMAIN_DENY"]. The process gets exactly these on top of the short list below, when they are set, and aamio addon list shows what was asked for. Never AAMIO_HOME.
hostsOptional, true or an object. The core then gives the add-on allow HOST, forget HOST and list, keeps the user's list in addons/NAME.hosts.json in the home, and lets an argument or tool field marked host through only for a website on it, asking the user in a form over MCP otherwise. legacy_file names a file in the home from an earlier version to read in once.
commandsName to help, arguments, needs and runtime. An argument is name (positional, or --option), help, required (options), host (true for the website the user must have allowed), subject (partner or message: the core resolves the name to a key). needs: ["own_key"] passes the runtime's public key. runtime: false runs without a home, as a check of a local file beside a running server.
toolsMCP tools as the client sees them: name, description, inputSchema (type object; no oneOf at the top, which hosts refuse), optional annotations; plus host (the field with the website), subject (an object of partner and message to the fields that carry them; exactly one is given), needs.

The process

One request per process. The core starts it, writes one JSON document to a file the process reads as its standard input, and waits for the deadline at most while watching what it writes. The process answers with one JSON document on standard output and exits. Standard error goes to the user on the command line and is dropped over MCP. The environment is a short list: the path, a temporary directory, the locale, the interpreter's own settings, TLS roots, the proxy variables, and AAMIO_ADDON=2. The probe at enable receives this baseline environment, which can include proxy credentials, since the process needs the same way out as the core. On ordinary calls the add-on also receives any set variables named in its manifest's environment, which aamio addon list shows before the add-on is enabled; a variable the manifest does not name, a token in the user's environment, say, never reaches the process. An answer is at most 1 MB and standard error at most 64 KB: a process that writes past either, or past its deadline, is stopped while it runs, and the files of the call, kept in a private directory of the call's own, are removed. At most two calls of one add-on run at once and twenty start per minute.

{
  "aamio_addon": 2,
  "kind": "command",
  "name": "check",
  "arguments": {
    "domain": "firma.no",
    "partner": "Kari",
    "name": null
  },
  "subject": {
    "key": "<43 characters>",
    "subject_source": "partner",
    "partner": "Kari",
    "signature_checked_locally": null
  },
  "own_key": null,
  "state_dir": "~/.aamio/addons/domain",
  "deadline_seconds": 30,
  "aamio": {
    "version": "0.6.27",
    "runtime": "python"
  }
}

kind is command, tool or probe. The probe is what enable sends before anything is recorded: enabling an add-on starts its process once, there and then, with the short list of environment variables and none of the manifest's, to see that it answers as its manifest says; the answer must be {"result": {"ok": true, "name": NAME, "version": VERSION, "aamio_addon": 2}}. subject is the key the core resolved, or null; state_dir is a directory of the add-on's own, or null for a command with runtime: false.

{
  "result": {
    "status": "listed",
    "...": "..."
  },
  "exit_code": 0,
  "is_error": false,
  "stderr": "a line to the user, optional"
}

result is printed as JSON on the command line and is the tool result over MCP; raw, base64 of bytes, goes to standard output as it is instead, for a file that must be byte for byte the same everywhere; exit_code is the command line's; is_error sets isError over MCP. Nothing in an answer changes state in the core: an add-on that says listed adds no partner, allows nothing and sends nothing.

Write one

Python. A distribution that depends on aamio>=0.6.27, with a package that holds aamio-addon.json and a module to run, and the entry point [project.entry-points."aamio.addons"] NAME = "your_package". Declare the manifest as package data. For a checkout, AAMIO_ADDONS=your_package names it without installing.

PHP. A Composer package of type aamio-addon that requires aisenseapi/aamio>=0.3.13, with extra.aamio-addon = {"name": NAME, "contract": 2}, aamio-addon.json and the process file at the package root. The process gets no autoloader from the core: load your own classes. For a checkout, AAMIO_ADDONS=/path/to/the/package names it.

Then aamio addon list shows it, aamio addon enable NAME probes it and records the choice, and aamio addon NAME --help comes from the manifest. The example add-on hello is in both client repositories, under examples/hello, tested with the core and not published: Python, PHP. Its process is a dozen lines.

The official add-ons

An add-on from anyone else is theirs: the core says which code runs and keeps the key out of its hands, and the user decides whether to trust it, as with any package.

What is not promised

Contract 2 is for add-ons the user trusts. With contract 2, the core sends only the public key needed for the call. The add-on runs with your file permissions and the same network, as the same user: nothing stops it from reading a file the user can read. Limits set by the operating system, approval bound to a package version and its hash, and an install path that runs no package code are the next step, taken when add-ons from third parties come. enable binds to the package name today, and says all of this when it is run.