Skip to content

Devices

A device is a worker we did not raise. Added in AGNT2-267.

Everything the product had run on the tool channel until then was a pod: we created it, we gave it a pass minted for one session, and it lived for a few minutes. A device is the other kind of peer on that same channel — a thing somebody owns, that sits in a house, that is switched on for months and off for a week, and that dials in by itself.

Why it works at all

Because of the first structural fact on the tool channel page: the worker calls out, and nobody calls in. That was written for a pod, and it happens to be exactly the property a Raspberry Pi behind a home router needs. It cannot be reached from outside, and it does not have to be.

This is not new architecture. It is the existing one, turned around.

What it shares with a session — which is most of it

The wire, entirely: the subprotocol, the JSON-RPC framing, the MCP handshake, the tools/list answer, the tools/call correlation and the close codes. All of it is the tool channel, unchanged, and one class runs it for both kinds of peer. The device slice contains no protocol code at all.

What it does not share, and the price of that

A session is defined by its ceilings — ttlSeconds, maxExecSeconds, the idle window the reaper reads, the seven states, the six failure reasons, the four teardown doors. A device has none of them, so it is its own table rather than a session with nine columns made nullable.

The price is stated rather than discovered:

  • There are now two kinds of peer on one channel. Every future reader of the runtime group has to ask "session or device?" where there used to be one answer, and someone debugging a socket has two tables to look in.
  • Four machines do not apply to a device. The profile catalogue (how big), the deadline reaper (when it dies), teardown's four doors (how it is put away) and the six failure reasons (why a start went wrong) are all session machinery. A reader who assumes the runtime group has one lifecycle will be wrong.

The cheaper option lost for a concrete reason: making a session indefinite turns nine of its columns into "null here means this row is not really a session", and every reader — the reaper, the sweep, the teardown, the state screen — has to learn a second row shape. One nullable column too few and the reaper deletes somebody's camera because it has been idle for ninety seconds.

The row is also the credential

publicId + secretHash, the same split the embed key uses: what is stored is not a working key.

The difference from an embed key is what the credential names. There, a key names a team, and a team may have any number of them. Here the key is the device's identity, one per row — because revoking has to stop one device, and because the channel has to know which device just dialled.

The key hangs off the team rather than an agent, so a deleted team cannot leave a working credential behind it.

The routes

GET    /teams/:teamId/devices                       every device a team owns
GET    /agents/:id/devices                          the ones this agent may call
POST   /agents/:id/devices                          attach one
POST   /agents/:id/devices/:deviceId/disconnect     drop the live connection
DELETE /agents/:id/devices/:deviceId                detach it

Where the code is

the device itself, its channel, the keyapi/src/slices/runtime/device
attaching one to an agent, its toolset, call rateapi/src/slices/agent/device

See also Use and Tool channel.