Reviewing Debian packages with an AI agent through a controlled gateway (interstellar + a debian-packager wormhole)

Hi all,

I’ve been building a small side project to make my own Debian packaging review workflow less tedious, and I’d love feedback from people who do this for real — both on the idea and on whether it’s actually useful or not.

The short version

interstellar is an MCP server that an AI agent (Hermes Agent, in my case) connects to. Instead of handing the agent raw shell access, its capabilities come from wormholes — small, purpose-built plugins that each expose a few typed operations rather than a generic “run this command” tool. The gateway in the middle enforces a capability-based policy and writes an audit log of everything the agent does.

For packaging work I wrote a debian-packager wormhole (it lives in the companion wormholes repo, alongside a testflinger one). So the agent can do things like executing review (which has steps defined), building source package, building binary packages, linting etc. In a deterministic way.

The design stance is deliberately narrow: wormholes expose specific typed operations, not a shell. Anything that would accept a caller-supplied command is denied by default until I explicitly opt in. The point is to get the convenience of an agent driving the boring parts of a package review without giving it the keys to the machine.

About the review steps (this is where I most want eyes)

The debian-packager wormhole doesn’t just shell out to lintian — it walks the agent through a set of explicit review steps, one file each:

Two honest tensions I want to be upfront about:

  • Some steps overlap with what lintian already checks. That’s deliberate, not an oversight. Lintian tells you that a tag fired; it doesn’t guide agent (for the most part, some info messages are really good though). The steps exist to steer the agent toward a good remediation for specific warnings/errors — the “here’s how we’d actually resolve this, and the reasoning” layer that a raw tag doesn’t carry.
  • Some steps are opinionated. symbols files are the clearest example (like C++ mangling pain hint to the agent). I’ve baked my own preferences in, and I know reasonable maintainers may disagree here.

So, specifically:

  • Which steps are redundant with lintian without adding fix guidance, and should just defer to it?
  • Where are my opinionated calls (symbols especially) wrong, or wrong-as-a-default even if fine for my own packages?
  • Which warnings/errors are missing a step where a bit of agent guidance would genuinely help?
  • Which operations would actually save you time in a real review? What would you like to see added?

What it looks like in practice

Here’s an actual review session with the agent driving the debian-packager wormhole:

status
section1
section2

And here’s what the gateway logs on the server side. Two things stand out: the plumbing wormholes (ssh, tailscale, testflinger, contained-debdev) all load with tools=0 — they provide transport and targets, not things the agent can call — while debian-packager is the only one exposing agent-callable tools.

On startup it loads the wormholes and serves MCP:

msg="wormhole loaded" name=debian-packager  version=main tools=5
msg="wormhole loaded" name=contained-debdev version=main tools=0
msg="wormhole loaded" name=testflinger      version=main tools=0
msg="wormhole loaded" name=tailscale        version=main tools=0
msg="wormhole loaded" name=ssh              version=0.1.0 tools=0
msg="serving MCP over HTTP" addr=0.0.0.0:8420 version=main

Then the agent calls the review tool, and the gateway brings the whole chain up on demand — Tailscale, SSH jump, a Testflinger hardware reservation, and finally a clean build container — before running the checklist inside it:

msg="link up"       target=tailnet    wormhole=tailscale       port=tailnet
msg="link up"       target=jump       wormhole=ssh             port=target
msg="link log"      target=hw-amd64   message="testflinger job 82fac5fa… submitted; waiting for the reserved machine (provisioning can take ~45 min)"
# … ~29 min of provisioning elided (including one transient Testflinger OIDC retry) …
msg="link log"      target=hw-amd64   message="reserved machine ready: ubuntu@10.x.x.x (expires 2026-07-08T20:57:56Z)"
msg="link up"       target=hw-amd64   wormhole=testflinger     port=reserved
msg="link log"      target=build-amd64 message="installing contained + podman runtime"
msg="link up"       target=build-amd64 wormhole=contained-debdev port=exec

# chain is up — the review checklist runs inside the container (~3 min):
msg="wormhole log" tool=review message="running review checklist on …/amdsmi@… (several minutes per step)"
msg="wormhole log" tool=review level=warn message="gbp export-orig produced no orig; falling back to uscan"
msg="wormhole log" tool=review message="step patch_headers warned: 2 patch(es) missing DEP-3 fields"
msg="wormhole log" tool=review message="step signed_tags warned: debian/watch does not verify upstream signatures (pgpsigurlmangle/pgpmode)"
msg="wormhole log" tool=review message="step dh_python warned: Python package without dh-python in debian/rules"
msg="wormhole log" tool=review message="step copyright_lrc warned: lrc reported discrepancies (exit 3)"
msg="wormhole log" tool=review message="step copyright_holders warned: compare the two lists above"
msg="wormhole log" tool=review level=warn message="step watch failed: uscan --report failed (exit 1) — watch file broken or upstream unreachable"
msg="wormhole log" tool=review message="step ppa_build_warnings warned: build warnings O=4 D=0 C=1 across archs (amd64, amd64v3)"
msg="wormhole result received" tool=review content_bytes=17856 is_error=false

# same session, next tool call — the chain is still warm, so no re-provisioning:
msg="wormhole log" tool=build_binary_package message="building binary package from …/amdsmi@…"
msg="wormhole log" tool=build_binary_package level=warn message="build warnings: O=2 D=0 C=1 in amdsmi_7.2.4-1~26.04.1_amd64.build"
msg="wormhole result received" tool=build_binary_package content_bytes=5498 is_error=false

Note the second call didn’t re-reserve or re-provision anything — the Testflinger reservation and the container stay up for the idle_timeout window, so a review followed by a build (and however many more) all ride the same warm chain.

When the reservation deadline or idle timeout finally hits, every link tears itself down again (link down … reason="idle timeout"), so hardware isn’t held longer than needed.

Trying it yourself

The easiest way to run it is the Docker Compose deployment, which wires up config, secrets, persistence, and optional wormhole installers:

git clone https://github.com/talhaHavadar/interstellar
cd interstellar/deploy/interstellar-mcp

cp .env.example .env      # secrets (Tailscale auth key, SSH creds, ...)
$EDITOR config.yaml       # define your targets (see below), and possible update docker-compose.yml

docker compose up -d      # gateway comes up on http://127.0.0.1:8420

Adding a wormhole is just uncommenting its installer service (and the matching depends_on entry) in the compose file — the installer images copy the binary into a shared volume, no local building. The gateway endpoint has no built-in auth; it’s bound to loopback on purpose. Reach it from elsewhere via an SSH tunnel, a Tailscale ACL, or a reverse proxy that adds auth — not by binding it to 0.0.0.0.

The compose file itself is mostly wiring — here it is, trimmed of the commented-out extras:

services:
  # Wormhole installers: each copies its binary into a shared volume and exits.
  # Enable one by uncommenting it here AND under interstellar.depends_on.
  install-contained-debdev:
    image: ghcr.io/talhahavadar/wormhole-contained-debdev:latest
    volumes: [extra-wormholes:/out]
    restart: "no"
  install-debian-packager:
    image: ghcr.io/talhahavadar/wormhole-debian-packager:latest
    volumes: [extra-wormholes:/out]
    restart: "no"
  install-testflinger:
    image: ghcr.io/talhahavadar/wormhole-testflinger:latest
    volumes: [extra-wormholes:/out]
    restart: "no"

  interstellar:
    image: ghcr.io/talhahavadar/interstellar:latest
    restart: unless-stopped
    depends_on:
      install-contained-debdev: { condition: service_completed_successfully }
      install-debian-packager: { condition: service_completed_successfully }
      install-testflinger: { condition: service_completed_successfully }
    # No built-in auth — bound to loopback on purpose (see note above).
    ports:
      - "127.0.0.1:8420:8420"
    command:
      - --listen=0.0.0.0:8420
      - --config=/etc/interstellar/config.yaml
      - --audit-log=/var/lib/interstellar/audit.jsonl
      - --wormhole-dir=/var/lib/interstellar/wormholes # built-ins
      - --wormhole-dir=/opt/interstellar/wormholes # installed extras
    volumes:
      - ./config.yaml:/etc/interstellar/config.yaml:ro
      - ./id_ed25519:/opt/interstellar/.ssh/id_ed25519:ro # your SSH key, keep private
      - interstellar-data:/var/lib/interstellar # persists the audit log
      - extra-wormholes:/opt/interstellar/wormholes:ro
    env_file: [.env] # secrets live here, not in config
    security_opt:
      - no-new-privileges:true

volumes:
  interstellar-data:
  extra-wormholes:

The interesting part, though, is config.yaml. A target is something the agent can act on; targets can ride on top of other targets via via:, and anything marked visible: false is hidden from the agent, it’s pure plumbing. Here’s my actual packaging setup, sanitized and trimmed to one build target (I run three: two amd64, one arm64):

targets:
  # What the agent SEES: build/lint/review on ubuntu-devel.
  # Real hardware underneath, but to the agent it's just a target.
  build-amd64:
    wormhole: contained-debdev # runs steps inside a clean container
    port: exec
    config:
      image: ghcr.io/talhahavadar/contained-debdev:ubuntu-devel
      runtime: podman
      ensure_deps: true
      install_runtime: true
    open_timeout: 60m # waits on the hardware reservation below
    idle_timeout: 3h # reserve once, build/lint/review many times
    via:
      host: hw-amd64

  # --- everything below is hidden plumbing (visible: false) ---

  hw-amd64: # reserve a real machine via Testflinger
    wormhole: testflinger
    port: reserved
    visible: false
    config:
      job_file: /path/to/testflinger-amd64.yaml
      ssh_keys: ["gh:YOURHANDLE"]
      reserve_timeout_secs: 21600
    open_timeout: 60m
    idle_timeout: 1h
    via:
      orchestrator: jump

  jump: # SSH jump host into the lab
    wormhole: ssh
    port: target
    visible: false
    config:
      host: JUMP_HOST
      user: YOURUSER
      key_file: /opt/interstellar/.ssh/id_ed25519
      insecure_skip_host_key_check: true # deliberate opt-out for this host
    via:
      net: tailnet

  tailnet: # userspace Tailscale; authkey from env
    wormhole: tailscale
    port: tailnet
    visible: false
    config:
      hostname: interstellar-gw
      authkey_env: TS_AUTHKEY

policy:
  deny_capabilities: [exec.arbitrary] # no generic shell, ever
  wormholes: {}

So the agent is only ever offered “build/lint/review on ubuntu-devel (amd64/arm64)”. The Testflinger reservation, the SSH jump, and the VPN are all visible: false — it rides the chain without being able to see or address the hosts. And exec.arbitrary is denied globally, so even a compromised agent can’t get a generic shell. Secrets (the Tailscale key, SSH creds) come from .env, never from config.yaml.

Then point your agent at it — for Claude Code:

claude mcp add --transport http interstellar http://127.0.0.1:8420

For Hermes Agent:

# add this in config.yaml
mcp_servers:
  interstellar:
    url: http://127.0.0.1:8420
    timeout: 3600

Ask it to call interstellar__status first to confirm your wormholes, tools, and targets loaded (it also tells you why a tool is hidden if policy or a missing target excludes it).

Full compose docs: https://github.com/talhaHavadar/interstellar/tree/main/deploy/interstellar-mcp

Repos (both Apache-2.0 / MIT):

Thanks for reading — happy to answer anything.

5 Likes

I don’t have any experience with Ubuntu “production” processes but, based on my past experience within a corporate development environment, I love the “spirit” and “feel” of what you have put together! I don’t know enough to comment beyond that.

Well done!

:slight_smile:

1 Like

Thank you for your comments @ericmarceau

1 Like