Skip to content

[Dogfooding] Linux device control is reliable, but shell/exec is too coarse for safe agent operations #108

Description

@Disdjj

Summary

I used Tool Bridge to deploy Home Assistant, an official Xiaomi integration, Docker Compose services, and a Cloudflare Tunnel on a remote Ubuntu device without using SSH directly.

The device connection was stable and the deployment succeeded. I did not observe dropped commands or unexplained offline transitions. The core Linux-device path is genuinely useful.

The main friction is that the only general process interface is an arbitrary shell command:

device/<linux>/shell/exec
effect: destructive
confirm: true
scope: call
input:  { command: string, cwd?: string, timeoutMs?: number }
result: { stdout, stderr, exitCode }

This is a correct conservative classification for arbitrary shell, but it is too coarse to be the primary agent operations layer. Read-only inspection and high-impact mutations look identical to the caller, output is unstructured, and long-running work has no observable lifecycle beyond a timeout and final stdout/stderr.

Environment

  • tb CLI: 0.25.0
  • Gateway: 0.17.0
  • Device: x86_64 Ubuntu 26.04 LTS
  • Device connection: Linux daemon / WebSocket
  • Shell exposure: explicit allowed commands: *
  • Device URL, credentials, usernames, domain names, and Home Assistant data are intentionally omitted

What worked

Through the device connection I was able to:

  • inspect OS, CPU, memory, disk, Docker, and Compose state;
  • create deployment directories and configuration;
  • clone and install the upstream Home Assistant integration;
  • start, stop, and restart Docker Compose services;
  • inspect container health and logs;
  • configure and verify a secure tunnel;
  • repeat state checks after mutations.

The connection remained available throughout the deployment.

The device also exposes a structured filesystem context at device/<linux>/fs. That is a useful existing capability, not a missing feature. It supports list/get/write/update/delete/search and optimistic concurrency via ifVersion.

Friction observed

1. Read-only and mutating shell commands have the same contract

All of these are represented as the same destructive, confirming tool:

uname -a
docker ps
docker compose ps
docker compose up -d
docker compose down
# and any arbitrary file/process mutation

This creates two bad choices for an agent workflow:

  • request confirmation for every harmless diagnostic read; or
  • obtain broad authorization for a sequence where the live contract cannot distinguish inspection from mutation.

The answer should not be heuristic parsing of shell strings. Shell grammar, aliases, redirections, scripts, and subprocesses make reliable effect inference unrealistic.

A safer direction is to retain arbitrary shell/exec as destructive while allowing the device to expose predeclared structured commands with explicit schemas and effects, for example:

  • system information / disk / process / Docker status as effect: read;
  • service restart / Compose apply as effect: write;
  • deletion / destructive lifecycle operations as effect: destructive, confirm: true.

The current daemon already requires explicit shell allowlisting. Extending the exposure model with per-command metadata or structured argv operations would preserve that safety boundary.

2. The result is final raw text, not an operation lifecycle

The only result is stdout, stderr, and exitCode. The schema does not expose:

  • commandId / operation identity;
  • started/completed timestamps;
  • live stdout/stderr streaming;
  • output truncation metadata;
  • poll/status/cancel;
  • whether a timeout killed the process;
  • reconnect/resume behavior.

Docker pulls, image starts, log inspection, and package/integration setup can take long enough that an agent needs observable progress. In this deployment I worked around this by keeping commands bounded and issuing separate verification commands. No command timed out, but the interface made long-running steps harder to supervise than necessary.

3. Every mutation needs a second verification call

Raw shell success only proves a process exit code. It does not prove the intended deployment state.

After each meaningful operation I had to run additional commands such as Compose status, container logs, HTTP checks, or file reads. This is appropriate validation, but common high-level operations could return structured postconditions rather than forcing every caller to rebuild them from text.

4. Filesystem support is useful but incomplete as a configuration workflow

The existing device/<linux>/fs surface is better than shell redirection:

  • write is an idempotent full replace;
  • update supports optimistic ifVersion;
  • delete is explicitly destructive.

However:

  • live help currently omits effect for list/get/write/update even though their scopes are read/write;
  • update replaces content rather than applying a line/hunk patch;
  • there is no atomic multi-file change set;
  • multi-line Compose/YAML changes still make shell-based editing tempting.

An explicit patch operation with ifVersion, plus an optional atomic batch for several configuration files, would make configuration changes safer and easier to review.

5. Shell has no SecretStore injection boundary

The shell schema accepts only a command string, cwd, and timeout. It has no way to request a SecretStore reference as an environment variable without materializing the secret in device configuration, a command, or an intermediate file.

Arbitrary stdout/stderr also cannot guarantee that a process will not print credentials. During this deployment I had to deliberately avoid commands that displayed token, certificate, or tunnel credential contents.

A first-class secretRefs -> env mapping should:

  • resolve secrets at execution time;
  • avoid placing values in argv, help, audit text, or result payloads;
  • redact exact injected values from stdout/stderr;
  • expire or zero the injected process environment after the operation.

Suggested direction

Keep arbitrary shell dangerous

  • Preserve shell/exec as destructive + confirm:true.
  • Keep default shell exposure disabled.
  • Do not infer safety from command text.

Add a structured command exposure profile

Allow Linux device owners to expose named operations with:

  • executable + argv schema (no implicit shell unless explicitly requested);
  • cwd and environment policy;
  • effect, confirm, timeout, output limits;
  • optional structured result parser/postcondition;
  • per-operation allowlist and audit identity.

Add observable execution

For long-running operations:

  • return an operation/command ID immediately;
  • provide status, streaming output, cancel, deadline, and terminal result;
  • distinguish timeout, cancellation, process signal, disconnect, and normal exit;
  • treat a disconnected or timed-out mutation as outcome unknown unless a terminal state can be recovered.

Extend the existing filesystem context

  • emit explicit read/write effects for list/get/write/update;
  • add patch-with-ifVersion;
  • consider atomic multi-file batches;
  • keep path confinement and no-follow protections.

Add SecretStore-backed environment injection

  • pass only secret references through the control plane;
  • inject values directly into the child environment;
  • redact injected values from logs/results;
  • never persist them in device exposure config or command history.

Suggested acceptance criteria

  1. A device can expose at least one structured read-only system command with effect: read and no destructive confirmation.
  2. Arbitrary shell/exec remains destructive and cannot be downgraded by command-name heuristics.
  3. A long-running command can be started, observed, cancelled, and resolved to a terminal state by ID.
  4. Timeout/cancel/disconnect outcomes are machine-readable and safe retry policy is explicit.
  5. Filesystem read/write tools expose correct effect metadata.
  6. A file patch can be guarded by the current ifVersion.
  7. A secret can be injected by reference without appearing in argv, logs, results, or persistent device config.
  8. Existing Linux daemon shell/FS exposure remains backward compatible.

Related historical context: #71 and #72 discuss device-call observability and CLI round trips on mobile. This issue is specifically about the Linux arbitrary-shell operations layer observed during a successful real deployment.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions