Skip to main content

vta_cli_common/commands/
device.rs

1//! `device …` operator commands (online, via the trust-task dispatcher).
2//!
3//! Thin wrappers over the `VtaClient::device_*` methods. A personal AI agent
4//! (open-claw / nano-claw / hermes) is a Service consumer
5//! (`consumerKind.serviceKind = "aiAgent"`); these commands enroll, list, and
6//! retire its `DeviceBinding`. See `docs/02-vta/personal-ai-agents.md`.
7
8use serde_json::{Value, json};
9use vta_sdk::client::VtaClient;
10
11use crate::render::{DIM, RESET, is_json_output, print_json};
12
13/// Print a trust-task result either as raw JSON (`--json`) or as a labelled,
14/// pretty-printed document for humans.
15fn print_result(label: &str, value: &Value) -> Result<(), Box<dyn std::error::Error>> {
16    if is_json_output() {
17        print_json(value)?;
18    } else {
19        println!("{label}");
20        println!("{}", serde_json::to_string_pretty(value)?);
21    }
22    Ok(())
23}
24
25/// The camelCase spelling `device/_shared/0.2` gives an enum value
26/// (`ai-agent` → `aiAgent`, `cache-and-keys` → `cacheAndKeys`), so an operator
27/// who types the 0.1 spelling is not refused by the VTA's schema.
28fn camel(value: &str) -> String {
29    let mut out = String::with_capacity(value.len());
30    let mut upper = false;
31    for ch in value.chars() {
32        match ch {
33            '-' => upper = true,
34            c if upper => {
35                out.extend(c.to_uppercase());
36                upper = false;
37            }
38            c => out.push(c),
39        }
40    }
41    out
42}
43
44/// `device list` — list the caller's registered devices. `service_kind` filters
45/// to a single Service consumer kind (e.g. `aiAgent`) when supplied.
46pub async fn cmd_device_list(
47    client: &VtaClient,
48    service_kind: Option<String>,
49) -> Result<(), Box<dyn std::error::Error>> {
50    let mut filters = json!({});
51    if let Some(sk) = service_kind {
52        filters["consumerKindFilter"] = json!("service");
53        filters["serviceKindFilter"] = json!(camel(&sk));
54    }
55    let result = client.device_list(filters).await?;
56    print_result("Registered devices:", &result)
57}
58
59/// `device register` — claim a `DeviceBinding` for the authenticated DID. The
60/// DID must already be in the ACL (provision the `ai-agent` template first).
61pub async fn cmd_device_register(
62    client: &VtaClient,
63    service_kind: String,
64    display_name: String,
65    platform: Option<String>,
66    hpke_public_key: Option<String>,
67) -> Result<(), Box<dyn std::error::Error>> {
68    let consumer_kind = json!({ "kind": "service", "serviceKind": camel(&service_kind) });
69    let result = client
70        .device_register(
71            consumer_kind,
72            &display_name,
73            platform.as_deref(),
74            hpke_public_key.as_deref(),
75        )
76        .await?;
77    println!("{DIM}Device registered.{RESET}");
78    print_result("Binding:", &result)
79}
80
81/// `device disable` — disable a device by id (kept on record, can no longer
82/// authenticate). The operator kill switch.
83pub async fn cmd_device_disable(
84    client: &VtaClient,
85    device_id: String,
86) -> Result<(), Box<dyn std::error::Error>> {
87    let result = client.device_disable(&device_id).await?;
88    println!("{DIM}Device {device_id} disabled.{RESET}");
89    print_result("Result:", &result)
90}
91
92/// `device wipe` — remotely wipe a lost/compromised device. Marks it wiped +
93/// disabled and records the reason. `scope` is `cache`, `cacheAndKeys`, or
94/// `full`.
95pub async fn cmd_device_wipe(
96    client: &VtaClient,
97    device_id: String,
98    reason: String,
99    scope: String,
100) -> Result<(), Box<dyn std::error::Error>> {
101    let scope = camel(&scope);
102    let result = client.device_wipe(&device_id, &reason, &scope).await?;
103    println!("{DIM}Device {device_id} wiped (scope: {scope}).{RESET}");
104    print_result("Result:", &result)
105}
106
107/// `device set-wake` — record the device's push `WakeHandle` (gateway DID/URL +
108/// opaque handle) and return the trigger allowlist.
109pub async fn cmd_device_set_wake(
110    client: &VtaClient,
111    gateway: String,
112    handle: String,
113    suggested_triggers: Vec<String>,
114) -> Result<(), Box<dyn std::error::Error>> {
115    let result = client
116        .device_set_wake(&gateway, &handle, suggested_triggers)
117        .await?;
118    print_result("Wake handle recorded:", &result)
119}
120
121/// `device heartbeat` — refresh `lastSeenAt`; returns server time + any queued
122/// operations for the device.
123pub async fn cmd_device_heartbeat(
124    client: &VtaClient,
125    platform: Option<String>,
126) -> Result<(), Box<dyn std::error::Error>> {
127    let result = client.device_heartbeat(platform.as_deref()).await?;
128    print_result("Heartbeat:", &result)
129}