feat(service,host): a Mac command carries a kind

Merged
DA
10d ago
+15891-167

PR MB of the "Browser agent tab automation setup" rollout (Mac path, M5). This PR gives a Mac command a kind with typed params, so later PRs can send the Mac a browser command. shell is the only kind that runs. run_on_mac behaves exactly as before.

What changed

  • Database (20261020221922_mac_command_kinds). The migration is additive and online-safe:
    • mac_command.kind text NOT NULL DEFAULT 'shell'. A constant default is a catalog-only change in PG 11+, so every row already standing reads as shell.
    • mac_command.params jsonb, nullable.
    • command drops NOT NULL, since a browser row has no shell text. This is also catalog-only. The table only holds rows while their calls wait.
    • A new table mac_command_listener_kind (user_id, kind, seen_at), keyed by (user_id, kind).
    • Every statement is idempotent.
  • Wire (mac-command-wire.ts, contract/mac-commands.ts):
    • MAC_COMMAND_KIND, and macCommandSchema as a union: shell {command} | browser {params}. params is any JSON whose text is at most MAX_BROWSER_PARAMS_CHARS (100k). It stays opaque until MD defines it.
    • The Mac's read decodes a kind this build doesn't know as {id, kind} alone, using the same pattern as chat's unknown block types, so one unknown command doesn't spoil the read.
    • A result is now shell result | {refused}.
  • Version tolerance:
    • The held read names the kinds the host runs in a kinds query (?kinds=shell,browser). An older desktop sends none, which means shell only. Shell is always included, and a kind the service doesn't know is ignored, never refused.
    • The service claims and waits only on rows of the named kinds. A browser row neither wakes nor gets claimed by an older Mac's read.
    • Each read marks the account listening, and also marks each kind it named beyond shell. If no Mac that runs browser commands listened recently, a browser call gets not-run with a reason at once instead of timing out. With two Macs on different versions, each is counted for the kinds it runs.
    • The host dispatches by kind: shell runs as before. Any other kind (browser for now, or an unknown one) is settled with {refused}, and the call returns that as not-run with the Mac's reason.
  • Service: askMac(userId, ask) is the generic path for any kind of command. runOnMacTool sends it a shell ask, and ME will call it for browser asks. This PR adds no new agent tool.

Tests

  • Store, against a real dialect:
    • A read naming shell only neither waits on nor claims a browser row, and a read naming browser claims it.
    • A row inserted without a kind, the way a pre-migration build wrote one, reads as shell and is claimed as a shell command.
    • Listening is tracked per kind, and an older Mac's read doesn't wipe a newer Mac's browser mark.
  • Route, byte for byte:
    • An older Mac's read (no kinds) gets only the shell row, and the browser row stays waiting until a newer read takes it.
    • A browser call with only an older Mac listening gets not-run at once, and nothing is left queued.
    • A browser command the Mac refuses comes back not-run with the Mac's reason.
  • Host: each read names [shell]. Out of a shell, a browser and an unknown-kind command, only the shell one is spawned. The other two are settled as refusals, and the reads carry on.
  • Client: the read sends kinds=shell,browser, and decodes an unknown kind as {id, kind}.

Out of scope: image results (MC), browser driving (MD), and the tool's routing (ME).

Made with Luke


Devin Review