8gent Code.Docs
Device Kit (8DK), Preview

Capabilities as agent tools

Each allowed capability becomes one tool, and every call passes a fixed series of checks before it reaches the device.

DeviceToolAdapter turns allowed capabilities into tools the agent can call, and runs every call through the checks below.

const adapter = new DeviceToolAdapter(registry, (deviceId) => links.get(deviceId), {
  onDecision: (d) => record(d), // optional: one report per call, allowed or not
});

The second argument finds the live connection for a device. A device with no connection is refused as not connected.

Tool names

device__<device id, with - as _>__<capability>

desk-lamp and set_light give device__desk_lamp__set_light. Device ids never contain _ and capability names never contain __, so the name always splits back the same way. deviceToolName and parseDeviceToolName do the conversion.

What the agent sees

adapter.toolDefinitions() lists allowed capabilities of paired devices, and nothing else. A capability nobody allowed is not in the list. For the test lamp in packages/8dk/__tests__/fake-device.ts, with set_light allowed:

{
  "type": "function",
  "function": {
    "name": "device__desk_lamp__set_light",
    "description": "[DEVICE Desk lamp (lamp) actuator] Turn the lamp on or off and set its brightness.",
    "parameters": {
      "type": "object",
      "properties": {
        "on": { "type": "boolean", "description": "true for on" },
        "brightness": { "type": "number", "description": "0-100" }
      },
      "required": ["on"],
      "additionalProperties": false
    }
  }
}

This is the same shape as 8gent's built-in tools. Adding these definitions to the agent's tool list in a running session is slice 2.

The checks, in order

adapter.execute(tool, input, ctx) copies the input first, so nothing changed afterwards reaches the person or the device. Then:

  1. Real device, real capability. The name has to resolve to a paired device and one of its capabilities.
  2. Allowed. The person granted this capability. No policy rule can lift this check.
  3. Policy. The permission engine is asked about action device_use, with agentId, sessionId, deviceId, capability and capabilityKind. A block refuses the call. Background candidate agents (shadow runs) are always refused, granted or not.
  4. Input. The input matches the capability's params: no unknown keys, required keys present, right types. A person is only ever asked about a request of the right shape. In v1 string values are not checked for control characters or length, so an approval prompt must escape every value it shows. Refusing them is part of slice 2.
  5. Ask. If a policy rule says to ask, or the capability is confirm, the person is asked through ctx.approve. No approver means no.
  6. Still allowed. After any wait for the person, the device has to be the same pairing and the capability still allowed.
  7. Connected. The device has a live connection and answers in time (10 seconds by default, ctx.timeoutMs).

Any refusal returns { ok: false, reason } and the device is never called. The reason starts with [8dk-deny] for 8DK's own checks and [policy] for the permission engine and for any approval the person refused or nobody could answer, including confirm. A call that reached the device and failed there returns { ok: false, reason: "device error: ..." }.

Why allowed capabilities do not also ask by default: the grant already is the person's yes. Asking again on every read would train people to click yes without reading. Asking every time is opt in, per capability (confirm) or per rule.

Your own rules

Rules for device_use go in ~/.8gent/policies.yaml, in the same format as 8gent's other policy rules, and apply on top of the grant:

version: 1
policies:
  - name: "ask-before-lamp-actions"
    action: device_use
    condition: "deviceId equals desk-lamp and capabilityKind equals actuator"
    decision: require_approval
    message: "Lamp actions need a yes"

Fields a condition can match: deviceId, capability, capabilityKind (sensor or actuator), agentId, sessionId. block refuses, require_approval asks.

The device side

serveDevice(definition, send) answers device:invoke frames with the device's handlers and sends back device:result. A handler that throws becomes an error result; it never stops the device loop. A capability name the device has no handler for is answered with an error.

Frames

DirectionFrames
Device to vesseldevice:hello, device:auth, device:result, device:event
Vessel to devicedevice:pairing, device:paired, device:invoke, device:revoked

parseDeviceFrame turns untrusted input into a frame or null. CallCorrelator matches each device:result to its call by callId and times out calls nobody answers. In v1, device:event is parsed but nothing delivers it to the agent yet.

Next: Security model.