Write an extension
An extension is a program the node starts and talks to over stdin and stdout. The pointman-extension crate speaks that protocol for you, so your code only declares commands and answers them. New extensions are compiled: one small binary per platform, with nothing to install on the machine.
Start from the template
Section titled “Start from the template”pointman-extension-template is a
working extension to start from: one command, its tests, CI, and a release workflow that builds every
platform. On GitHub, Use this template makes a copy. Name it pointman-<id>, rename hello
everywhere (the package, the id in extension.toml, and the command names), and write your own
commands. The rest of this page walks through what’s in it.
The folder
Section titled “The folder”pointman-greeter/ extension.toml # what it is and what it adds Cargo.toml src/main.rs # its commands tests/greeter.rs # its tests, against a stand-in nodeName the repo pointman-<id>, with the topic pointman-extension. Extension repos are MIT by
default.
[extension]id = "greeter"name = "Greeter"version = "0.1.0"publisher = "acme"summary = "Writes a greeting to a file."platforms = ["macos-arm64", "macos-x86_64", "linux-x86_64", "linux-arm64"]
[run]command = ["bin/greeter"] # the release build, in the package's bin/runtime = "binary"
[[commands]]kind = "greeter.greet"title = "Greet"params = { type = "object", required = ["name"], properties = { name = { type = "string" } } }lane = "main" # commands in a lane run one at a time
[settings]type = "object"
[settings.properties.greeting]type = "string"default = "Hello"[package]name = "greeter"version = "0.1.0"edition = "2021"license = "MIT"publish = false
[dependencies]pointman-extension = { git = "https://github.com/pointman-ai/pointman-extension", tag = "v0.1.0" }anyhow = "1"serde_json = "1"Every table and field is in the extension.toml reference.
A command
Section titled “A command”use anyhow::Context;use pointman_extension::{Extension, Job};use serde_json::json;
fn main() -> anyhow::Result<()> { let mut ext = Extension::new()?; let h = ext.handle();
ext.command("greeter.greet", move |job: &Job| { let name = job.params["name"].as_str().context("name is missing")?; let greeting = h.settings()["greeting"].as_str().context("no greeting setting")?.to_string(); let text = format!("{greeting}, {name}!"); job.progress(0.5, "writing"); let out = job.work.join("greeting.txt"); std::fs::write(&out, format!("{text}\n"))?; job.output(&out, "text", "main", json!({})); // posted where the request came from Ok(json!({ "text": text })) });
ext.run()}job.paramsare the command’s parameters, already checked against itsparamsschema.job.progressshows how far along it is, in the thread and inrequest_status.job.outputposts a file it made, in the thread that asked.h.settings()is this machine’s settings for it, with the defaults from[settings].- Log with
h.log(…), never by printing: stdout is the node’s. - An error ends the request as failed, with its message.
Agents ask for it with run_on_machine, or with a tool of its
own if you add a [[tools]] table.
Test it
Section titled “Test it”The crate’s stand-in node starts your built binary the way a real node does:
use pointman_extension::testing::StandIn;use serde_json::json;
fn greeter() -> StandIn { StandIn::new(env!("CARGO_MANIFEST_DIR")).binary(env!("CARGO_BIN_EXE_greeter"))}
#[test]fn greet_writes_the_greeting() { let node = greeter().settings(json!({ "greeting": "Hi" })).start().unwrap(); let done = node.run("greeter.greet", json!({ "name": "Sam" })).unwrap(); assert_eq!(done.result, json!({ "text": "Hi, Sam!" }));}
#[test]fn a_missing_name_fails() { let node = greeter().start().unwrap(); assert_eq!(node.run("greeter.greet", json!({})).unwrap_err().message, "name is missing");}cargo testThe stand-in also answers vault secrets, within what [permissions] allows.
Try it on a machine
Section titled “Try it on a machine”cargo build --releasemkdir -p bin && cp target/release/greeter bin/greeterpointman add .pointman add with a folder installs it on this machine’s node as it is, and turns it on. Run it
from any thread, then pointman add . again after each change.
Release it
Section titled “Release it”Bump version in extension.toml, then push a tag with that version:
git tag v0.1.1 && git push origin v0.1.1A release carries one build per platform in platforms, as assets named
<id>-<version>-<platform>.tar.gz, each with extension.toml, bin/<id> and the rest of the
package’s files at its top. The template’s release workflow does this when you push the tag: it
builds each platform on its own runner, Linux with musl so the binary runs on any distribution, and
publishes the release once every asset is attached.
When the release is published, your core takes it into your workspace’s catalogue, and machines
install it with pointman add greeter and move to new versions with pointman upgrade. That needs:
- the board’s GitHub App installed on the repo’s owner (see Projects);
- the repo’s owner among the GitHub accounts your core takes extensions from. That’s
pointman-aiunless your core setsPOINTMAN_EXTENSION_OWNERS, a comma list, where*means any account the App is installed on.
Core checks each build, refuses a release missing one, and keeps every version. Each node fetches its own platform’s build and checks its hash.