Ports: swap the system, keep the core
Muster talks to four kinds of outside system: an issue tracker, a code forge, a model provider and an agent runtime. Each sits behind a small interface. The core sees only the interface, never a vendor client or a wire format.
The core, the ports and the adapters
Swapping one tracker or forge for another means writing one adapter and a fake. The reducer and the tables do not change.
The contract, in plain words
- A pure core. The bridge imports only the standard library. Adapters import the bridge, so a dependency never points from the core outward.
- Named for the core's need. A port interface is small and is named for what the core needs, not for what the vendor offers. It takes a context and typed values.
- Every write has an idempotency key. A retry is harmless.
- Typed errors. The caller can tell a rate limit from a missing item from a refused request.
- Time and randomness are inputs. They are not globals.
- Adapters do not decide. They translate a call and its result.
- One client for each system. All traffic to a system goes through its adapter, the one place for rate limit backoff, conditional requests and a request budget.
- Credentials are not the adapter's. A credential is a header set once on the shared HTTP client, so an adapter never sees a token.
What an adapter provides
- The port interface, in the core's vocabulary.
- The adapter package, with a
doc.gothat follows the naming rules and a short README for the port. - A constructor that takes the shared HTTP client and a base URL.
- A fake in the test kit that satisfies the same interface. No fake is in a production file.
- Synthetic or anonymized fixtures, so a test never carries a real name, host or credential.
- A row in the structure table with passing and failing cases for its import rule.
How a new adapter is added and tested
The tests of the signalman, bosun and tally use the fakes, so they run fast, offline and the same way every time.
Webhooks in, API reads to check
The tracker and the forge send webhooks, and the gangway verifies, dedupes and records each one. The tally reads the tracker and the forge through the ports when a row is older than a set time, and records what it finds as events. A caller never decides from a stale row.
Where the ports are going
Direction. Everything in this section comes from designs the owner has approved. It is planned, and none of it is shipped.
One contract and a conformance suite
A small kit will give every port one shape: describe, health and close, six typed errors and a registry. A contract suite will run against every registered port and every fake. Core packages will import only the kit and the kind packages, so deleting an adapter directory leaves the build and the core tests green. Vendor names will stay inside their own adapter directory, and the inspect command will go red when one leaks.
A second forge adapter, Gitea, will prove it: one directory, one settings block and one registration line, with no core file changed. An authoring guide, with a skeleton that compiles, will let someone who has never seen the code add a port.
Webhooks with API fallback on every port that listens
Each instance of a port that receives vendor events will be set to one of three modes. In webhook mode the vendor pushes, and a reconcile pass still finds anything missed. In poll mode Muster reads the vendor on a cursor and serves no inbound route. In auto mode the webhook is primary, Muster falls back to polling when the channel goes silent or fails, and it returns after clean checks. You can set any mode at any time, in settings or with one command, with no restart.
The kit will own signature checks, replay and duplicate defence, ordering and the poller, so no adapter writes them. Delivery will be at least once, with duplicates dropped. The forge and the tracker will support these modes. The model provider and the agent runtime have no inbound events and need none.
A secrets port
Secrets will become a port too. It will give versioned reads and rotation without a restart. Files stay the default store, and Vault will be the first remote store. The port will keep the last good value for a bounded age, pause new work when the store is down, and never put a secret value in a log, an event, an error, a settings page or the audit log. One test will plant a marker secret and scan all of them.
The same design leads to per consumer provider keys, so each consumer holds its own key with its own limits.
Pluggable skiff backends
A six method backend interface, a registry and a router will let several backends run at once. The router uses fixed field rules and the first match wins. Local hosts and Kubernetes come first, in the tree. A driver protocol, a versioned JSON exchange with an external program, will add a cloud with no Muster code. Backend credentials will be references to files in the secrets directory. The seat protocol, the lease and the run credential do not change, and one run never spans two backends.
Licensing
Muster is Apache 2.0 today. The plan adds a contributor agreement with a bot gate, a trademark policy and a licence check on dependencies, so the project stays open to outside contribution.
Port by adapter by status
| Port | Adapter | Status | What it does |
|---|---|---|---|
| Issue tracker | Linear | Shipped | reads issues by state; comments and state writes with an idempotency key; creates issues; sets the delegate of a ticket and writes agent sessions |
| Code forge | GitHub | Shipped | reads the repository, pull requests, CI runs and one file; the head of the default branch |
| Model provider | OpenRouter | Shipped | reads key information |
| Agent runtime | pi, as an operating system process | Shipped | starts a process from a spec, waits for its exit, stops it and its children |
| Shared HTTP client | httpkit | Shipped | timeout, retry with jitter, Retry-After, ETag cache, request budget |
| Code forge | Gitea | Planned | a second forge, to prove the contract with one directory and one registration line |
| Tracker and forge | webhook, poll and auto modes | Planned | webhooks with API fallback, set for each port instance |
| Secrets | file store and Vault | Planned | versioned reads and rotation without a restart |
| Skiff backends | pool, local, Kubernetes, exec driver | Planned | several backends live at once, chosen by a router |
| Notifier | to be chosen | Planned | a fifth kind of port |
| Every port | request quota for each decision class | Planned | a budget per class, not only per client |
Shipped means merged and working. Planned means approved in a design, with the build ahead.