a2acast 0.16.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
a2acast-0.16.1/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 James Gagan
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,411 @@
1
+ Metadata-Version: 2.4
2
+ Name: a2acast
3
+ Version: 0.16.1
4
+ Summary: Zero-infrastructure messaging between AI agent sessions on different machines (ntfy.sh wake pings + your shared repo as the payload channel)
5
+ Author-email: James Gagan <james.gagan@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/husker/a2acast
8
+ Keywords: agents,a2a,claude,chatgpt,copilot,ntfy,messaging,multi-agent
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Topic :: Communications
13
+ Requires-Python: >=3.8
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Dynamic: license-file
17
+
18
+ # a2acast
19
+
20
+ **Messaging between AI agents on different machines — no server, no
21
+ accounts, no open ports.** One stdlib-only Python file. End-to-end
22
+ encrypted. Claude Code on a Linux laptop, ChatGPT (Codex CLI) on a MacBook,
23
+ Copilot on a Windows PC — all exchanging messages and
24
+ [A2A](https://a2a-protocol.org) tasks.
25
+
26
+ ## Quick start (two machines, one minute)
27
+
28
+ **Machine A** — create the mesh:
29
+
30
+ ```bash
31
+ pipx install git+https://github.com/husker/a2acast # or: uv tool install ...
32
+ mesh init home # prints a block to paste on machine B, then starts listening
33
+ ```
34
+
35
+ **Machine B** — paste the block `mesh init` printed. It looks like:
36
+
37
+ ```bash
38
+ curl -fsSLO https://raw.githubusercontent.com/husker/a2acast/vX.Y.Z/mesh.py
39
+ python3 mesh.py join mesh1-XXXX...
40
+ ```
41
+
42
+ That's it: downloaded, joined (named after its hostname), listening.
43
+ Machine A prints `MESH_NODE_JOINED` the moment B joins.
44
+
45
+ **Talk** (from a new terminal, on either machine):
46
+
47
+ ```bash
48
+ mesh send all "hello mesh" # B's watcher prints it about a second later
49
+ mesh ping <b-name> # → MESH_PONG node=<b-name> rtt=~400ms
50
+ mesh ask <b-name> "run the tests and summarize failures" --wait 300
51
+ ```
52
+
53
+ No machine list to declare up front: **any machine with the join code can
54
+ join**, picks its own name, and every node learns about it automatically.
55
+ Share the join code privately — it IS the mesh secret.
56
+
57
+ ## Using it with Claude Code, Codex, or Copilot CLI
58
+
59
+ Install the plugin (teaches sessions the protocol and auto-reminds them
60
+ when a project is a mesh node):
61
+
62
+ ```
63
+ # Claude Code
64
+ /plugin marketplace add husker/a2acast
65
+ /plugin install a2acast
66
+ mesh claude-setup # once per project — arms presence at session start
67
+
68
+ # Codex CLI / ChatGPT desktop
69
+ codex plugin marketplace add husker/a2acast
70
+ codex plugin add a2acast@a2acast
71
+ mesh codex-setup # once per machine — arms presence at session start
72
+
73
+ # GitHub Copilot CLI
74
+ copilot plugin marketplace add husker/a2acast
75
+ copilot plugin install a2acast@a2acast
76
+ mesh copilot-setup # once per project — pins the watcher to this node
77
+ ```
78
+
79
+ Each plugin loads the mesh safety rules at session start. Claude uses
80
+ asynchronous `Stop` with `asyncRewake`; Codex uses `Stop`. Copilot runs the
81
+ watcher as an **MCP server** (`mesh mcp-serve`, wired per project by
82
+ `mesh copilot-setup` — see below) that Copilot starts with the session and
83
+ stops when it ends — including Ctrl-C and
84
+ crash. Because it isn't an agent shell, the session shows no "working" spinner
85
+ while it listens. When a message arrives the server wakes the idle session on
86
+ its own (via MCP sampling) and the session handles it with the `mesh_pending`
87
+ / `mesh_reply` / `mesh_send` tools — a real turn, so a `MESH_TASK` gets done,
88
+ not just acknowledged. (The first time, Copilot may ask once to approve the
89
+ server for sampling; approve it and later wakes run silently.)
90
+
91
+ The loop each session runs:
92
+
93
+ 1. With the plugin, follow the harness-specific setup above. Claude and Codex
94
+ need no manual watcher; on Copilot, run `mesh copilot-setup` once in the
95
+ project (Copilot hands a plugin MCP server no project info and there's no
96
+ portable way to guess it, so this pins the node in a workspace
97
+ `.github/mcp.json`). After that its MCP-server watcher listens and wakes the
98
+ session automatically — nothing to arm. Handling happens out of band (no
99
+ "working" spinner); the next prompt you send opens with a one-line note of
100
+ anything a2acast handled while you were away.
101
+ 2. Do your work. After pushing something the other machine should act on:
102
+ `mesh send <node> "one-line summary — pull"`.
103
+ 3. When a `MESH_TASK` line arrives, do the work and answer with
104
+ `mesh reply <task-id> "<result>"`.
105
+
106
+ Works with any number of nodes; each node has an inbox topic and `all`
107
+ broadcasts. `mesh claude-setup` registers the a2acast presence watcher by
108
+ writing the project's `.mcp.json` — the CLAUDE.md protocol snippet itself
109
+ now lives at `mesh integrate --format claude`. (You still run `mesh
110
+ init`/`mesh join` once per machine either way: the plugin teaches sessions
111
+ the protocol, it doesn't create the mesh. On Copilot you also run `mesh
112
+ copilot-setup` once per project. The plugin's
113
+ hooks and MCP server invoke the `mesh` CLI on your PATH — the same one
114
+ `mesh init` installed — so it works the same on macOS, Linux, and Windows.
115
+ On Windows, private state relies on your account's ACLs rather than POSIX
116
+ file modes, and evidence files are opened with verified-identity checks in
117
+ place of kernel `O_NOFOLLOW`; the full test suite runs green on
118
+ windows-latest in CI. Keep it current: `pipx upgrade a2acast` (or `uv tool
119
+ upgrade a2acast`) when you update the plugin.)
120
+
121
+ **Autonomous Codex nodes (opt-in).** Claude and Copilot wake an idle session
122
+ in-process to handle a `MESH_TASK`; Codex has no such path, so a joined Codex
123
+ machine can instead run a background **supervisor** that executes delegated
124
+ tasks with no session open. Enable it with `mesh codex-setup --supervise`,
125
+ then name the peers you trust to run code on this machine with
126
+ `mesh codex-allow <node>`. Nothing runs until you do both — the supervisor is
127
+ off by default and its allowlist starts empty. See the security model below.
128
+
129
+ ## Machine-wide worker pool (opt-in)
130
+
131
+ The worker pool runs repository tasks through local Codex, Copilot, and
132
+ Goose/Ollama CLIs. Run it on a joined worker host only after all three CLIs are
133
+ installed; authenticate Codex and Copilot for the current user, and start
134
+ Ollama with the configured model available (default `qwen3:4b`). The
135
+ coordinator must already be a current known mesh identity; confirm it with
136
+ `mesh status` before setup.
137
+
138
+ ```bash
139
+ mesh pool-setup --workspace-root ~/Projects \
140
+ --coordinator jamess-macbook-air-2
141
+ mesh pool-start
142
+ mesh pool-status
143
+ mesh delegate auto "add a regression test" --repo /abs/repo --wait 300
144
+ ```
145
+
146
+ `pool-setup` permits repositories only below the listed workspace roots and
147
+ sets `exec_allow` to the single named coordinator. It does not start a worker;
148
+ `pool-start` is the explicit activation step. Because every mesh member has
149
+ the shared key and can assert any sender name, this allowlist is not per-node
150
+ cryptographic identity. Configure only a coordinator you trust.
151
+
152
+ On macOS, `pool-start` manages current-user LaunchAgents. On other operating
153
+ systems, `pool-start` and `pool-stop` print foreground supervisor commands for
154
+ you or your service manager to run; they do not install a service.
155
+
156
+ For normal `auto` jobs, dispatch selects the first eligible backend in
157
+ Goose/Ollama, Copilot, then Codex order, skipping workers that are blocked,
158
+ busy, unavailable, or cooling down. Nonblocking CLI calls (the default
159
+ `--wait 0`) and the nonblocking MCP tool dispatch that one worker and do not
160
+ auto-redispatch. With a positive `--wait`, the CLI can try the next eligible
161
+ backend only after an authenticated `quota` or `unavailable` result, within
162
+ the same total wait budget. Security and integration jobs select only Codex
163
+ unless a backend is explicitly named.
164
+
165
+ Each job runs in a separate Git worktree. A worktree prevents checkout
166
+ collisions; a worktree is not a security sandbox. Worker processes still have
167
+ the local user's OS permissions, and repository tasks are untrusted input.
168
+ Results report an outcome, branch, commit, worktree, summary, and verification.
169
+ A branch and commit contain proposed production changes; they are not
170
+ integrated until you review and merge or cherry-pick them yourself.
171
+
172
+ The pool creates worktrees and local commits. Its worker instructions forbid
173
+ merge, push, PR, deploy, publish, and worktree deletion, and a2acast performs
174
+ none of those as automatic postprocessing. Cleanup is an explicit command:
175
+
176
+ ```bash
177
+ mesh pool-clean --integrated-into main
178
+ mesh pool-stop
179
+ ```
180
+
181
+ Normal cleanup removes only terminal, clean worktrees with consistent durable
182
+ records whose commits are integrated into the named ref; uncertain or
183
+ unintegrated work is preserved. `mesh pool-clean --task <id> --force` is an
184
+ explicit escape hatch for exactly one terminal task and may discard its
185
+ unintegrated or dirty worktree.
186
+
187
+ ## How it works
188
+
189
+ One file, `mesh.py`, Python stdlib only. Messages travel through an
190
+ [ntfy](https://ntfy.sh) relay (default: the public ntfy.sh; self-host with
191
+ `mesh init --server`) over **outbound-only HTTPS connections on both ends**
192
+ — which is why two laptops behind NAT can talk with no port forwarding, no
193
+ VPN, and no server of yours. Topics are derived from the mesh secret and
194
+ the node name, so nothing is ever registered anywhere; delivery latency is
195
+ about a second. `mesh watch --follow` holds one streaming connection and
196
+ prints each message as it lands.
197
+
198
+ In a terminal, `init` and `join` flow straight into that watcher when they
199
+ finish — programs calling mesh (scripts, agent harnesses; anything without
200
+ a TTY) get the plain return-immediately behavior instead.
201
+
202
+ ## Delegating tasks: any AI talking to any AI
203
+
204
+ Nodes don't just ping each other — they exchange real
205
+ [A2A protocol](https://a2a-protocol.org) tasks in JSON-RPC envelopes:
206
+
207
+ ```bash
208
+ mesh ask desktop "run the test suite and summarize failures" --wait 300
209
+ # → MESH_TASK_RESULT from=desktop state=completed: 2 failures, both in auth...
210
+
211
+ # on the receiving side (its agent sees this via `mesh watch --follow`):
212
+ # MESH_TASK from=laptop task=5e52304e... state=submitted: run the test suite...
213
+ mesh reply 5e52304e "2 failures, both in auth: ..."
214
+
215
+ mesh tasks # ledger of everything asked/answered
216
+ mesh card desktop # its A2A agent card
217
+ ```
218
+
219
+ And because the wire format is real A2A, `mesh a2a-serve` runs a
220
+ **localhost bridge** so any A2A-capable framework (LangGraph, Google ADK,
221
+ Microsoft Agent Framework, …) can talk to remote mesh nodes as ordinary A2A
222
+ servers — discovery via agent cards, `message/send`, `tasks/get`:
223
+
224
+ ```bash
225
+ mesh a2a-serve # → http://127.0.0.1:4737/agents/<node> per remote node
226
+ ```
227
+
228
+ See [docs/AGENTS.md](docs/AGENTS.md) for per-harness wiring (Codex CLI,
229
+ Copilot CLI, Gemini CLI, A2A frameworks, cron). By default, `mesh` only moves
230
+ messages; the explicitly started worker pool is the exception and invokes its
231
+ configured local CLI. Interactive nodes answer with whatever brain, tools,
232
+ and permissions their own harness has.
233
+
234
+ ## Security model (read this)
235
+
236
+ **Messages are end-to-end encrypted and authenticated.** The mesh key is
237
+ generated by `mesh init`, lives only in `.meshwire.json` on your machines,
238
+ and travels only inside join codes you share yourself. On the wire, the
239
+ relay (and anyone who discovers a topic) sees ciphertext, topic id, size,
240
+ and timing — nothing else. Sender and recipient names ride *inside* the
241
+ ciphertext.
242
+
243
+ Construction (stdlib-only, standard primitives): HKDF-SHA256 key derivation
244
+ → HMAC-SHA256 PRF in counter mode for encryption, encrypt-then-MAC with an
245
+ independent HMAC-SHA256 key, random 128-bit nonce per message, and
246
+ constant-time tag comparison. The `mw2` authentication tag also binds the
247
+ mesh id, exact relay topic, and send timestamp. Envelopes older than seven
248
+ days (or implausibly far in the future) are rejected, and authenticated
249
+ ciphertext fingerprints are persisted per node so duplicate relay deliveries
250
+ and replays are suppressed across restarts. `mw1` remains readable during
251
+ rolling upgrades but new sends always use `mw2`. Unauthenticated, stale,
252
+ misrouted, replayed, or tampered messages are **dropped, not displayed**.
253
+
254
+ What you still must do:
255
+
256
+ - **Guard the join code and `.meshwire.json`** — they contain the key. Both
257
+ are auto-gitignored; config writes use mode `0600`; share join codes over a
258
+ private channel. The key is never placed in an environment variable:
259
+ `A2ACAST_CONFIG` contains only a path. MCP tools, agent cards, and the local
260
+ A2A HTTP bridge never return config or key fields.
261
+ - A join code grants **full membership in one trust domain**: its holder can
262
+ decrypt traffic, publish authenticated traffic, and mint the same sender
263
+ names as any other member. There are no per-node cryptographic identities
264
+ or roles. `exec_allow` limits autonomous execution, not mesh membership.
265
+ - **Treat inbound tasks as untrusted input.** Encryption authenticates *the
266
+ mesh*, not intent: any agent (or person) holding the key can send tasks.
267
+ Receiving agents should apply their normal permission rules.
268
+ - **Autonomous Codex execution is opt-in and gated on a curated allowlist.**
269
+ `mesh codex-supervise` runs delegated tasks through `codex exec` and replies
270
+ over the mesh. It stays off unless you start it (`codex-setup --supervise`),
271
+ and even then only peers you add with `mesh codex-allow` run — a
272
+ default-empty allowlist (`exec_allow`), **not** the roster of everyone who
273
+ holds the key. The `--sandbox read-only` default is defense-in-depth, not
274
+ the boundary: a read-only task can still read repo secrets and return them
275
+ in its reply, so only allow peers you actually trust.
276
+ - Sender names prove a shared-key member made the assertion, not which member:
277
+ every node holds the same group key. a2acast rejects A2A metadata that
278
+ disagrees with its authenticated outer route, but a compromised member can
279
+ still choose another member's sender name in that outer route.
280
+ - Someone who learns a topic id (but not the key) can't read or forge
281
+ messages, but can post garbage that your watcher silently drops.
282
+ Self-hosting ntfy with auth (`mesh init --server https://ntfy.example`)
283
+ closes even that.
284
+ - Upgrade all machines together when moving to a new a2acast version —
285
+ it's one file. (v0.4 meshes interoperate; v0.4 clients just render the
286
+ new join/ping control messages as odd one-off messages.)
287
+
288
+ ### Incident response: revoke and rotate the mesh key
289
+
290
+ If a join code, config, or member machine is compromised, rotate both the key
291
+ and topic capability from one trusted node:
292
+
293
+ ```
294
+ mesh rotate-key
295
+ ```
296
+
297
+ That command switches new commands on the current node to a fresh key and
298
+ fresh topic id, then prints a private `mesh rotate-key mesh1-...` command. Run
299
+ that exact command on every remaining trusted node over a private channel. Do
300
+ not send it through the compromised mesh. Restart long-running `mesh watch`,
301
+ MCP/harness, and supervisor processes on each node so they drop their in-memory
302
+ copy of the old config. Nodes that have not applied the new code remain on the
303
+ revoked mesh and cannot read or publish on the new topics. Compare the
304
+ `key: sha256:...` line from `mesh status` out of band to confirm every trusted
305
+ node completed the cutover.
306
+
307
+ ## CLI reference
308
+
309
+ ```
310
+ mesh init <name> [--as NODE] [--server URL] create a mesh; in a terminal, prints the invite block and keeps listening
311
+ mesh join <code> [--as NODE] join from a code, announce, and (in a terminal) keep listening
312
+ mesh invite print the join code + paste-able bootstrap block
313
+ mesh rotate-key [mesh1-code] rotate the key/topics, or apply a peer rotation
314
+ mesh iam <node> set/change this machine's identity
315
+ mesh send <node|all> <msg...> [--intent request|inform|ack] [--reply-to ID]
316
+ message a node (or broadcast) with reply intent
317
+ mesh presence listening|working|blocked set and broadcast agent status
318
+ mesh watch --follow stream messages forever (preferred; background task)
319
+ mesh watch [--timeout N] one-shot: block until one message, print, exit
320
+ mesh ping <node> [--timeout N] liveness + round-trip time (answered by watchers)
321
+ mesh ask <node> <text...> [--wait SECS] delegate an A2A task
322
+ mesh run ensemble [--timeout N] -- "<prompt>" fan out and collate replies
323
+ mesh run cross-review [--timeout N] -- <diff-or-ref> two independent reviews
324
+ mesh reply <task-id> <text...> [--state completed|failed|...] answer one
325
+ mesh tasks [get <id>] task ledger
326
+ mesh tasks --wait <id> [--timeout N] wait for a terminal task result
327
+ mesh card [node] [--name N --description D] A2A agent card
328
+ mesh a2a-serve [--port 4737] [--wait 60] localhost A2A HTTP bridge
329
+ mesh peek [node] [--since S] show recent messages without consuming
330
+ mesh peek --wait [--from NODE] [--timeout N] wait for the next arrival
331
+ mesh status mesh, identity, known peers + last seen
332
+ mesh integrate [--format codex|copilot|claude|mcp|skill] print setup for a harness/route
333
+ mesh mcp [--config PATH] stdio MCP tool server for any MCP client (Claude Desktop, Cursor, …)
334
+ mesh claude-setup register the Claude Code presence watcher (writes .mcp.json)
335
+ mesh codex-setup [--supervise] arm Codex presence; --supervise also launches the autonomous actor
336
+ mesh copilot-setup register the Copilot CLI presence watcher
337
+ mesh codex-allow <node> [--revoke|--list] trust (or untrust) a peer to run delegated tasks here
338
+ mesh codex-supervise [--once] [--sandbox S] [--interval N] [--stop] the autonomous task actor
339
+ mesh pool-setup --workspace-root DIR --coordinator NODE [--model MODEL] configure the worker pool
340
+ mesh pool-start|pool-status|pool-stop manage or inspect worker supervisors
341
+ mesh delegate auto|codex|copilot|goose <task...> --repo ABS [--base COMMIT]
342
+ [--kind implementation|analysis] [--class normal|security|integration]
343
+ [--verify TEXT (repeatable)] [--wait 0..300] [--as NODE] run an isolated task
344
+ mesh pool-clean [--integrated-into REF] [--task ID [--force]] remove eligible worktrees
345
+ ```
346
+
347
+ The blocking commands use shell-friendly exit codes: `tasks --wait` exits
348
+ `0` for `completed`, `1` for any other terminal state, and `124` on timeout;
349
+ `peek --wait` exits `0` on an arrival and `124` on timeout.
350
+
351
+ `mesh run ensemble` creates one correlated task per other node in the dynamic
352
+ roster, waits for all terminal replies or the time window, then prints each
353
+ answer plus a no-reply list. `mesh run cross-review` prefers two available
354
+ peers (blocked nodes rank last), gives both the same independent review brief,
355
+ and collates their findings. Recipes exit `0` when every task completes, `1`
356
+ for dispatch or terminal failures, and `124` when any dispatched node does not
357
+ reply before the timeout.
358
+
359
+ New messages carry a stable message id and default to `inform` when intent is
360
+ missing (including messages from older clients). Use `request` when a reply is
361
+ required, `inform` for FYI traffic, and `ack` to close a loop; `--reply-to`
362
+ correlates a response to the displayed message id. Agent integrations follow a
363
+ fixed rule: always answer `request`, answer `inform` only when it adds value,
364
+ never answer `ack`, and do not send filler greetings or thanks.
365
+
366
+ Presence controls carry `listening`, `working`, or `blocked` through normal
367
+ announce/ping/pong/ack traffic and explicit `mesh presence` beacons. Session
368
+ and stop hooks maintain `working`/`listening`; approval integrations can run
369
+ `mesh presence blocked` while waiting for permission. `mesh status` and
370
+ `mesh_list_agents` show the latest reported state, and `mesh ask` warns before
371
+ sending to a blocked node.
372
+
373
+ To set a stable node name use `mesh iam <name>` (writes a per-harness pin).
374
+ Prefer this over the `A2ACAST_NODE` env var, which is not reliably inherited
375
+ by harness-spawned processes.
376
+
377
+ To keep the mesh key outside a project, point `A2ACAST_CONFIG` at an existing
378
+ config file. The explicit path takes precedence over ancestor discovery and is
379
+ inherited by lifecycle hooks:
380
+
381
+ ```bash
382
+ export A2ACAST_CONFIG=/absolute/path/to/mesh-node/.meshwire.json
383
+ # PowerShell: $env:A2ACAST_CONFIG = 'C:\path\to\mesh-node\.meshwire.json'
384
+ ```
385
+
386
+ `mesh claude-setup` and `mesh copilot-setup` still write their MCP workspace
387
+ files in the current project while pinning that isolated config path.
388
+
389
+ **Onboarding & MCP clients.** `mesh integrate` prints the right setup for
390
+ whatever you run — a harness plugin (`--format codex`/`copilot`), a CLAUDE.md
391
+ snippet (`--format claude`), a paste-in skill (`--format skill`), or the MCP
392
+ config (`--format mcp`). For GUI/desktop agents, `mesh mcp` runs a stdio MCP
393
+ **tool** server (add it with `mesh integrate --format mcp`) exposing
394
+ `mesh_send` / `mesh_pending` / `mesh_ask` / `mesh_reply` / `mesh_delegate` /
395
+ `mesh_list_agents`, so Claude Desktop, Cursor, or any MCP host can talk to
396
+ the mesh — no plugin needed. (This is the pull-mode tool server; the Copilot plugin's `mcp-serve`
397
+ is the push-mode watcher that wakes an idle session.)
398
+
399
+ ## How it compares
400
+
401
+ | | a2acast | shared MCP queue | SSH + headless agent | plain git polling |
402
+ |---|---|---|---|---|
403
+ | Infrastructure | none | server to run | SSH + reachable host | none |
404
+ | Wake latency | ~1 s | poll interval | seconds | poll interval |
405
+ | Payload channel | message or your repo | the queue | the SSH pipe | your repo |
406
+ | Audit trail | git history | custom | none | git history |
407
+ | N nodes | yes | yes | pairwise | yes |
408
+
409
+ ## License
410
+
411
+ MIT