Docs / Authoring packs

Author your own pack

A pack is a directory of YAML you write and maintain yourself — no SDK, no compiler, no signing keys, nothing uploaded to anyone. Drop it on the runners you control, trust it once, and your model can call it. Most packs stay private to your fleet, and that's by design: the public registry is curated, not a free-for-all (more on that below).

  1. 1

    Lay out the pack directory

    A pack is just a folder: a top-level pack.yaml manifest and one YAML per action in actions/. Optional scripts/ holds packaged shell scripts for kind: script actions. Keep the whole thing in your own git repo.

    my-pack/
      pack.yaml                    # pack manifest (see step 2)
      actions/
        do_thing.yaml              # one declared action per file
        do_other_thing.yaml
      scripts/                     # only when you have script actions
        do_thing.sh
  2. 2

    Write pack.yaml

    The manifest declares pack-level metadata and which action YAMLs to load. id is the pack slug (used in the install path and, for self-maintained packs, however you reference it internally).

    schema_version: 1
    id: my-pack
    name: My ops pack
    version: 0.1.0
    description: Short one-line summary shown on the runner + dashboard.
    vendor: acme
    homepage: https://github.acme.internal/sre/my-pack   # your repo; optional
    
    requires:
      os: [linux]
      binaries: [my-cli]      # optional — advertised to cloud; a missing one fails at run time
    
    actions:
      - actions/do_thing.yaml
      - actions/do_other_thing.yaml
  3. 3

    Declare each action

    One YAML per action — the runner refuses anything not in the catalog, so this file is the contract between your ops team and the LLM. This is where the safety lives: tight argument validation, an honest side_effects list, and the right risk tier. See the full YAML schema reference for every field.

    schema_version: 1
    id: my.do_thing
    title: Do the thing
    kind: exec
    risk: low
    
    description: >
      One-paragraph human description. Shown verbatim to the LLM —
      write it like a doc string, not a comment.
    
    side_effects:
      - Reads /var/log/myapp/*.
      - Touches nothing.
    
    args:
      - name: file
        type: path
        required: true
        validation:
          allowed_prefixes: ["/var/log/myapp/"]
    
    execution:
      command:
        binary: cat              # bare name — resolved via PATH on the host
        argv: ["{{ args.file }}"]
      timeout: 10s
    
    output:
      parser: text
      max_stdout_bytes: 65536
    
    # Optional. Renders on the dispatch form so operators (and LLMs)
    # see a real invocation before they fill in args.
    examples:
      - title: Tail myapp's error log
        args:
          file: /var/log/myapp/error.log
  4. 4

    Validate, install, and trust it

    Validate locally first — the same checks the runner runs at load — then install the directory into the runner's packs dir and reload:

    # on the runner host
    emisar pack validate ./my-pack
    sudo emisar pack install ./my-pack --dest /etc/emisar/packs
    sudo systemctl reload emisar

    Because your pack isn't one emisar publishes, it has no baseline hash — so it lands in the dashboard's Packs page as pending, with dispatch held until you approve it. Open Packs, review the content hash, and click Trust. From then on, that exact byte-for-byte version is the only one authorized: edit the pack and its hash changes, which re-marks it pending until you re-trust. That's your drift guard — trust in bytes you reviewed, not a publisher you hope is honest.

    Confirm it end-to-end: open the runner in the dashboard, check your actions under Advertised actions, and Run one.

  5. 5

    Roll it out and maintain it

    To put the pack on another runner, install it pinned to the hash you trusted, so every host runs identical bytes — or let your config management (Ansible, Chef, a base image) drop the directory and reload:

    sudo emisar pack install ./my-pack --hash sha256:<the hash you trusted> --dest /etc/emisar/packs
    sudo systemctl reload emisar

    When you change the pack, bump its version, re-validate, re-deploy, and re-trust the new hash once in the dashboard — the same review you'd give any production change. The pack is yours: its lifecycle, its hosts, its risk tiers, on your schedule.

Keep it yours — or propose it to the registry

The public registry behind the Packs page is curated by us, deliberately. We only take packs that are genuinely generic and broadly useful — Postgres, Cassandra, Docker, Linux core, the AWS suite — the kind of thing many teams run unchanged. We don't accept every submission, because an open dumping ground of one-off, environment-specific packs would be a mess: hard to trust, hard to maintain, and low signal for everyone browsing it.

Most packs should stay yours. A pack that hardcodes your service names, your hostnames, or your internal CLIs is more useful private — and you keep control of its lifecycle. Self-maintained is the default here, not a fallback.

If your pack really is generic — useful to teams who've never heard of yours, with no environment-specific assumptions — we'd love to ship it. Open a PR against the repo adding it under runner/examples/packs/ plus an entry in apps/emisar_web/lib/emisar_web/packs_registry.ex, or email support@emisar.dev first if you're not sure it clears the bar. We review for:

  • Genuinely generic — no hardcoded hosts, service names, or org-specific paths. If it only makes sense inside your company, it belongs in your repo, not ours.
  • Tight argument validation — no free-form shell strings; paths use allowed_prefixes, choice args use validation.enum, and numbers use min/max.
  • Honest side_effects — every file, network, and process side effect listed. The LLM reads these.
  • Correct risk level — destructive actions (restart, delete, repair) are high or critical; reads are low.
  • Resolvable binaries — declare what the runner needs on the host under requires.binaries.