Skip to content

Latest commit

 

History

History
156 lines (142 loc) · 9.37 KB

CHANGELOG.md

File metadata and controls

156 lines (142 loc) · 9.37 KB

Changelog

All notable changes to Machook are documented here. The format follows Keep a Changelog, and versions follow Semantic Versioning.

[Unreleased]

[0.2.1] — 2026-09-28

Changed

  • Async endpoints no longer have a timeout. The wall-clock limit exists to stop a synchronous caller hanging, and an async caller already holds its 201. Applying the endpoint's Timeout anyway killed exactly the long jobs async mode was turned on for: the log recorded a run the caller was told was accepted, then a second line 30 seconds later marking it 504 and SIGTERM'd. Async runs are still bounded by Max concurrent commands and by the output cap. The Timeout field still applies to synchronous endpoints and to the editor's Test button.
  • Execution log records an outcome. An async run's HTTP status is always 201, because that is what the caller was told, so statusCode could not distinguish an accepted run from a finished, failed, or killed one. Records now carry outcome (accepted, succeeded, failed, timed out, rejected), and the menu bar and Settings key their glyph off it. Older log lines without the field are decoded by inferring it, so an existing log still reads back.

Fixed

  • The endpoint editor's Timeout help text claimed the limit applied to async endpoints while saying it was ignored. It now states plainly that it has no effect while Async is on.

[0.2.0] — 2026-09-25

Added

  • Async endpoints. Each endpoint now has an Async toggle. When enabled, the HTTP caller receives 201 Created with a run_id immediately, and the command keeps running in the background. Async endpoints still respect timeout, output cap, and concurrency limits; they just don't make the caller wait. MCP tool calls also honor the flag and return an immediate accepted result.
  • Persistent execution log. Every run is appended to ~/Library/Logs/machook/executions.jsonl as a JSON line containing stdout, stderr, exit code, duration, status code, and an async flag. The in-memory Recent runs ring is now hydrated from this store on launch, so history survives relaunch and async completions remain inspectable. The log rotates at 10 MB.
  • Log viewer. Settings → General → Recent runs now shows an async tag and an Open log button that reveals the log folder in Finder.

[0.1.0] — 2026-09-10

First release. A macOS menu bar app that runs local shell commands in response to incoming webhooks.

Added

  • Endpoint table. Map a request path to a shell command, edited in Settings: methods, timeout, working directory, enable/disable. Endpoints resolve against the live configuration on every request, so changes take effect with no restart.
  • Request envelope. Every request is written to a private temp file (0600 in a 0700 per-user directory) as JSON — method, path, query, headers, parsed body, raw body — and the file's path replaces {{request}} in the command. The Authorization header is stripped before the file is written, the file is deleted when the command exits, and stale files are swept at launch. The path is also exported as MACHOOK_REQUEST_FILE.
  • Synchronous responses. stdout becomes the response body with a sniffed content type, the exit code becomes the status: 200 on success, 500 with stderr on a non-zero exit, 504 on timeout, 503 at the concurrency cap or for a disabled endpoint, 405 for a rejected verb, 404 for an unknown path. Runs report X-Machook-Exit-Code, X-Machook-Duration-Ms, and X-Machook-Truncated.
  • MCP server. POST /mcp publishes each endpoint as a tool, with the catalog generated from the live endpoint table. Tool calls execute the same command against the same envelope shape, with tool arguments delivered as the body. Per-endpoint tool name, description, input schema, and read-only annotation.
  • Cloudflare Tunnel. Bundled and supervised cloudflared in quick mode (ephemeral trycloudflare.com URL, no account) or named mode (stable hostname). The current URL is in the menu bar; click to copy.
  • Verified tunnel URLs. A published URL is not a working one: a quick tunnel is handed a hostname before its DNS record exists, and the gap can be minutes while every other signal looks healthy. Machook fetches <url>/health through the public URL — a Fibonacci warm-up over ~90 seconds, then every minute, then every five for as long as the tunnel is up — and reports the outcome in the menu, in Settings, and as tunnel_reachability on GET /status. A record that appears late flips the state to reachable on its own instead of leaving a stale failure on screen. Failures are named rather than generic: an edge with no connector (Cloudflare error 1033), a tunnel that reaches us but finds nothing listening, an Access policy in the way. A DNS failure is cross-checked against a public resolver over DNS-over-HTTPS, so "Cloudflare has not published the record" is distinguished from "the record exists and this Mac cannot see it" — usually macOS caching the failures from before the record appeared, which needs sudo dscacheutil -flushcache rather than a new tunnel. That cross-check runs before each probe, not just after a failure: asking the system resolver for a name that does not exist yet makes macOS cache the NXDOMAIN for the whole negative TTL, which broke the user's own curl to a tunnel that had come up fine.
  • Tunnel process hygiene. SIGTERM and SIGINT now quit the app through the normal path, so pkill no longer leaves a running menu bar app whose listener is dead. A cloudflared orphaned by a crash or a force-quit is terminated at the next launch, matched only by the exact executable path inside our own bundle so unrelated tunnels on the machine are never touched. If the listener stops without being asked to, the menu says so instead of looking healthy.
  • Readable tunnel logs. cloudflared output was recorded at debug level, which a default log show drops — the failure was invisible in exactly the case you would go looking for it. Errors, warnings, and connection loss are promoted to error or notice. The connector token is passed through the environment rather than --token, so it no longer appears in ps output.
  • Bearer token auth on every route except GET /health, compared in constant time. Settings refuses to save an enabled tunnel with an empty token.
  • Shell injection boundary. The command template from Settings is the only string the shell parses; network data arrives as a quoted argv element or a file path. Quoting only passes a value through unquoted when every character is provably inert.
  • Command runner with a wall-clock timeout (SIGTERM, then SIGKILL two seconds later), a per-stream output cap that keeps draining past the cap so the child never blocks on a full pipe, a concurrency budget, /dev/null on stdin, and an optional login shell so Homebrew and pyenv paths resolve.
  • Port fallback. The listener tries localAPIPort (7876) and then fallbackAPIPort (7877), since a fixed default port is a coin flip on a machine that already runs something there. It only moves before it has served traffic, the menu bar and Settings say when the fallback is in use, and the tunnel is started against the port that actually bound rather than the configured one. Set the fallback to 0 to insist on a single port.
  • Menu bar surface: tunnel URL, endpoint and tool counts, the last few runs, a note when the fallback port is in use, and a warning line when the HTTP listener cannot bind at all.
  • Built-in routes: GET /health (unauthenticated) and GET /status.
  • Sample endpoint. examples/log-request.sh records each request to ~/Library/Logs/machook/requests.log (0600, in a 0700 directory, rotated at 5 MiB) and answers with a JSON summary, so a new install can be exercised end to end before any real command is wired up. ?fail=1 exits non-zero on purpose to demonstrate the 500-plus-stderr mapping.
  • Sparkle auto-updates, Developer ID signing, and notarization in CI.
  • Universal builds. Releases ship one machook-universal artifact whose executable, bundled cloudflared, and Sparkle.framework each carry both arm64 and x86_64. A Sparkle appcast channel advertises a single <enclosure> with no architecture filtering, so a per-arch release would offer half the user base a build their Mac cannot launch; CI now fails if any shipped binary is missing a slice. The appcast's minimumSystemVersion is read from LSMinimumSystemVersion rather than hardcoded, so an update is never offered to a macOS version the app cannot run on.
  • 79 unit tests covering shell quoting, template rendering, path and tool-name normalization, endpoint validation, configuration decoding, envelope shape and file permissions, port-candidate selection, the port fallback against a real occupied socket, reachability classification, cloudflared log levels, stray process matching (including that another app's tunnel is never a candidate), DNS cross-check parsing and the verdicts derived from it, restart generation bookkeeping, listener shutdown reporting, and runner behaviour including timeouts, output caps, concurrency, and that injected metacharacters do not execute.