agmx
Run AgentMatrix work on your own machine, under your own coding-agent subscription.
AgentMatrix is a coordination service for coding agents: it plans work, holds
the review gates, and decides what lands. agmx is the other half — a small
client that picks up the work the server hands out and executes it locally,
inside a throwaway git worktree of a repository you already have checked out.
The server dispatches a project_id; your machine decides which directory
that is. When the runner enrolls it reports, per project, the checkout's path and
its remote URL — with any username, password or token stripped — so the server
can confirm the checkout belongs to that project. A project the server rejects
is printed and dropped for the session; if every project is rejected,
agmx runner start exits instead of polling for work it can never take.
Why it is separate
The control plane needs Postgres, Redis and a pile of orchestration code. A
runner does not. agmx depends on httpx and nothing else, so installing it
never asks you for a database URL or a shared secret.
$ pip list
agmx anyio certifi h11 httpcore httpx idna typing_extensions
Install
pipx install agmx
Requires Python 3.10+ and git on PATH.
Quickstart
# 1. Authenticate. Opens your browser at your team's sign-in page.
agmx login
# 2. Tell the runner where your checkouts live, then let it work.
agmx runner start --project agenticmatix=~/code/AgentMatrix
agmx login asks the server where your team signs in (its OAuth metadata) and
logs you in there with your browser: a loopback redirect to 127.0.0.1, PKCE
and a state check. On a machine you reach over SSH, agmx login --device
shows a code to enter on any device with a browser. agmx login --token takes a
personal access token instead (create one with manage_user create_token from a
logged-in coordinator CLI); it is read from the terminal, never from the command
line. The session refreshes itself; refresh tokens rotate, so each refresh is
saved at once and taken under a file lock, which lets two agmx processes share
one login. A server without single sign-on still gets --email and a password.
If the server refuses to enroll you (not_a_member, email_not_verified,
user_disabled, subject_mismatch, invite_expired), the runner prints the
server's answer and exits: those are for your team's admin, and retrying does
not change them.
--project may be repeated. A project_id you have not registered is reported
back as failed rather than guessed at, so a run can never be executed against
the wrong tree.
Point at a different control plane with --server, or AGMX_SERVER.
The first runner start enrolls the machine with your login and receives the
machine's own runner token, stored next to your login token. From then on
every call the runner makes — including enrolling again when it restarts — uses
that token, not your login session. If the server stops accepting it (the
runner was revoked, or you logged in elsewhere and re-enrolled), the runner
exits with one line telling you to agmx login and start again.
By default the server gives this machine only your own runs. --serve-team
(remembered; --no-serve-team to stop) lets it take your teammates' runs too —
they then run on the CLI subscriptions logged in on this machine.
On start, the runner removes worktrees that a runner which died mid-run left
behind in your registered checkouts — only its own (agmx-*/worktree in its
data directory, or in the temp directory where older versions put them, whose
owning process is gone), never yours or those of a runner that is still running.
Sandbox by default
The server builds every agent command to run inside that CLI's own sandbox. Full access — the agent can do anything your OS user can — is your choice, made on this machine:
agmx runner start --full-access ... # remembered in ~/.agmx/settings.json
agmx runner start --no-full-access ... # back to the default
With it off, the runner does not take the server's word for it: a command
carrying a flag that switches the sandbox or permission checks off
(--dangerously-skip-permissions, --allow-dangerously-skip-permissions,
--permission-mode bypassPermissions, --dangerously-bypass-approvals-and-sandbox,
codex --sandbox danger-full-access, qwen --yolo without --sandbox) is not
run; the run is reported failed before the agent starts.
At start it also checks, for each agent CLI on your PATH, that its sandbox
really works here, with a real probe rather than a guess, and prints exactly
what to install or configure when it does not: bwrap and socat for claude on
Linux, plus the kernel setting that Ubuntu 24.04+ uses to block the nested user
namespace claude's shell sandbox needs; a running docker or podman for qwen.
agy is not treated as sandboxed: its sandbox has not been measured anywhere
it starts and does not keep it from reading ~/.agmx, where the other runs'
tokens are, so agy runs are refused unless you choose --full-access.
Runs for a CLI whose sandbox cannot work are refused with that fix, instead of
quietly running unsandboxed; the other CLIs keep working. The runner never
edits your CLI settings files.
How a run executes
-
The runner leases one run:
{run_id, task_id, project_id, command, timeout_seconds, kind, base_ref, needs}. No path, no repository URL. -
It resolves
project_idagainst the checkouts you registered. -
The commits the run needs must be here. Every SHA in
needs(andbase_ref) is checked; any that is missing is fetched from theagmx-run/*branches on your remote — that is where another machine's run left it. Still missing means the run is reported as failed, naming the SHA. It is never run against some other commit instead. -
It creates a git worktree at
base_refwhen the server gave one (a fix round starts from the previous attempt; a review sits at the head it reviews), otherwise at the freshly fetched tip of the shared branch on the remote — never your local checkout, even when it is on a detached HEAD, so every machine starts from the same base. The shared branch is your current branch's upstream, or the remote's default branch when your checkout is detached — or the branch the server names for the project, when it names one. A repository with no remote uses local HEAD..agmx/is created inside it for the files an agent hands back to the server. -
The command runs there through
sh -c, withAGMX_WORKTREEset to the worktree's absolute path — some agent CLIs start their shell tool somewhere else and would otherwise go looking for the repository across your disk. The run's MCP server goes into~/.agmx/run-secrets/<run_id>/(0700, files0600) — inside~/.agmx, which the sandboxes the server builds for claude and codex refuse to read, so a sandboxed agent cannot read the token of another run on the same machine — in the form each CLI reads:AGMX_MCP_CONFIG(the lease's config verbatim, for--mcp-configof claude and qwen) andAGMX_AGY_DIR(agy,--add-dir: it holds anagmxplugin). qwen's sandbox is a container that mounts only the worktree,~/.qwenandTMPDIR, so the runner mounts that one run's directory into it, read-only, throughSANDBOX_MOUNTS. The run's token is exported asAGMX_MCP_TOKEN, and the command gets its own privateTMPDIR. All of it is deleted when the run ends — on timeout, cancellation or interruption too — and the token is masked in any output streamed back. An older server that sends no MCP still gets these, with no servers in them.The command does not inherit the runner's environment. It gets an allowlist:
PATH,HOME,USER,LOGNAME,SHELL,LANG,LC_*,TERM,TMPDIR,TZ,XDG_*, the proxy variables (http(s)_proxy,no_proxy, in both cases), CA bundles (NODE_EXTRA_CA_CERTS,SSL_CERT_FILE,SSL_CERT_DIR), and the agent CLIs' own configuration:ANTHROPIC_*,CLAUDE_*andCLOUD_ML_REGION(claude);CODEX_*andOPENAI_API_KEY(codex);AGY_*,ANTIGRAVITY_*,GEMINI_API_KEY,GOOGLE_API_KEY,GOOGLE_GEMINI_BASE_URL,GOOGLE_GENAI_USE_ENTERPRISE(agy);QWEN_*,OPENAI_BASE_URL,GOOGLE_CLOUD_PROJECT,GOOGLE_GENAI_USE_VERTEXAI(qwen). Anything else — cloud credentials for Bedrock or Vertex, say — is passed only when you ask:--pass-env NAME(repeatable), or"pass_env": [...]in~/.agmx/settings.json. The runner's own git commands keep the full environment, since pushing needs your SSH agent. If the repository has no git identity configured, the command also getsGIT_AUTHOR_*/GIT_COMMITTER_*asagmx runner <agmx@localhost>in its environment, so an agent never writesgit configinto your clone. Output is streamed back in batches while it runs, and every batch is delivered before the result is reported. The runner keeps sending heartbeats throughout, so a long run keeps its lease. -
What happens on exit depends on
kind:executeis the only kind that commits. The agent may commit by itself; whatever it leaves uncommitted is committed after it onagmx-run/<run_id>— never anything under.agmx/, which your repository does not ignore. The run's head is the worktree'sHEAD; one that does not descend from the base (the agent checked out something unrelated) fails the run instead of being reported as the work. The branch is pushed to your remote, because the reviewer is usually a different machine. A push that fails fails the run, saying the commit exists only on this machine; the local branch is kept. A pre-commit hook that rejects the change fails the run too, but the work is still committed to the branch so someone can see what was rejected. If the commit itself fails (a signing key that cannot be used, say), the worktree is kept on disk and its path is in the error, so the agent's uncommitted work can be recovered.- Every other kind (
review, and any kind added later) is read-only: no branch, no commit, no push. A review run's.agmx/review-<task_id>.jsonis read before the worktree goes and sent with the result — as an object when it is a JSON object, verbatim otherwise.
-
The worktree is removed and the runner reports facts: exit code, base and head SHA, whether it timed out. It does not decide whether the run succeeded; that judgement stays on the server, with the review history. The exit code is the agent's own:
0when the agent finished cleanly and a step of the runner's failed afterwards (commit, push), and absent when the agent never started (a missing commit, a worktree that could not be created) — so the server does not blame the agent for this machine's git. A report the server did not record (a 5xx, a dropped connection) is resent a bounded number of times; a refusal (4xx) is final.
A run that exceeds timeout_seconds has its whole process group terminated,
not just the process the runner spawned — coding agents spawn children. The same
happens at once when the server cancels a run (the heartbeat lists it, or the
server answers run_cancelled/not_your_run): nothing is committed or pushed,
the worktree is cleaned up, and the runner reports failed once.
How a result lands
The control plane keeps no checkout of your code, so it cannot merge and it does not pretend to. Once a result passes review, the server offers the merge to a runner that has registered that project, and the merge happens here:
- Fetch the shared branch (the one the server names for the project, else your
checkout's upstream), and check whether the commit is already on it — if
it is, the work is done and the runner says so. A commit this machine never
had is fetched from the
agmx-run/*branch the executing machine pushed. - Create a temporary worktree at the fetched remote tip and merge there
with
--no-ff. Your own checkout is never checked out, merged into, or required to be clean: a local branch that is behind, a detached HEAD, or uncommitted edits make no difference. - Push
HEADto the shared branch, then remove the temporary worktree. A merge that is not pushed leaves the work on exactly one laptop, and the merge commit reported back to the server would name something nobody else can see. - Delete the
agmx-run/*branches, locally and on the remote, that the landed commit now contains — only those, and a failure to delete never fails the landing. - Report the merge commit. Only then does the task become
done.
A repository with no remote is a single-machine project: the merge goes into the checked-out branch, which then has to be on a branch and clean.
Landing refuses rather than improvising, and says which kind of failure it
was, because that decides who waits: local — this machine's own state (not
a repository, the commit not found even after fetching, and without a remote a
detached HEAD or uncommitted tracked changes); another machine of the team may
still land it.
transient — the push timed out, or the remote moved ahead
(non-fast-forward, fetch first); retried shortly. conflict — a merge
conflict, or a push refused for any other reason (a protected branch, no
permission); only a person can fix those. Each failure is reported with the
reason, and your repository is left as it was — the temporary worktree goes
with the failed merge or the unpushed one, and the next attempt starts again
from the remote's new tip. Your uncommitted work is never touched.
The practical consequence: if nobody is running agmx runner start, no task
reaches done. That is the design, not an outage.
Configuration
| Path | Mode | Contents |
|---|---|---|
~/.agmx/.credentials.json |
0600 |
login session (access and refresh token) or personal access token, runner token |
~/.agmx/settings.json |
0644 |
server, email, issuer, runner_id, pass_env, full_access, serve_team |
~/.agmx/run-secrets/ |
0700 |
one directory per running run: its MCP config and agy plugin, removed when it ends |
~/.local/share/agmx/runs/ |
0700 |
one directory per run: its worktree, secrets, TMPDIR |
Secrets are kept in a separate file from settings, and the credentials file is
created with its mode already set rather than being widened and then narrowed.
Set AGMX_CONFIG_DIR to move the directory. Run directories live apart from
it ($XDG_DATA_HOME/agmx, or AGMX_DATA_DIR): the sandboxes the server builds
deny reads of ~/.agmx, which holds your tokens, and a worktree in /tmp would
be writable by every sandboxed run.
Security notes
- The password is read from the terminal only. There is deliberately no
--passwordflag: it would survive in shell history and inpsoutput. GIT_DIRandGIT_WORK_TREEare stripped from the environment handed to the command, and from the runner's own git calls, so neither an agent nor a merge can reach outside the repository it was pointed at. Those variables beatgit -C, always.- The default server is HTTPS.
agmx loginsends a password in the request body; plain HTTP would hand it to anyone on the path. - The runner token can drive only the runner routes: it cannot call a single coordinator tool, and revoking one machine does not touch your login.
- An agent never sees the runner's full environment (see How a run executes), and never writes into your checkout's git config: missing git identity is given to it through its environment.
Development
git clone https://github.com/nothan-agentic/AgentMatrix
cd AgentMatrix/runner
pip install -e .
python -m pytest tests -q
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file agmx-0.2.1.tar.gz.
File metadata
- Download URL: agmx-0.2.1.tar.gz
- Upload date:
- Size: 97.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a01bfa41fb937696802664a937597b0aa4e147deee391c84950af75862e15ca0
|
|
| MD5 |
b66132ae7aa065f0534c9ed871e1b3f0
|
|
| BLAKE2b-256 |
6d3fec74fa596fd8692deb436620a4676fcd505deac1032e72ef7f2f2153bb7d
|
File details
Details for the file agmx-0.2.1-py3-none-any.whl.
File metadata
- Download URL: agmx-0.2.1-py3-none-any.whl
- Upload date:
- Size: 56.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.12.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
97c2748af23f32aed6096918aed1a8688d7e17ca70194ef1790ac900338ddb7b
|
|
| MD5 |
49a4afebde8d85983cd3ee040aa1b925
|
|
| BLAKE2b-256 |
9f61f0caca8f6108ad1155bd2d01f562a88b935d929f8fc35ea963c5b5cbcc4a
|