Skip to main content

I Gave My Ray-Ban Meta Glasses Read-Only Access to My Homelab

·11 mins

A public messaging channel should not inherit the capabilities of a privileged infrastructure agent.

I wanted to ask my homelab a question while walking.

I did not want WhatsApp to become a remote administration terminal.

So the channel can inspect selected state, while known deployment and change tools are withheld from its runtime.

The demo looks simple. I dictate a message through my Ray-Ban Meta glasses, asking whether a service is healthy. A few seconds later, WhatsApp returns a live answer from my infrastructure agent.

Strictly speaking, the glasses do not inspect anything. They provide the voice interface to one constrained WhatsApp path.

Underneath that exchange is the part that actually matters: a public messaging channel crossing into a privileged agent system without creating a public route into my house, exposing an unrestricted shell, or trusting a prompt to enforce read-only access.

The glasses are only the interface. The capability boundary is the security control.

For example, I can dictate a question like this:

Perform a read-only live check of service-a.

The reply contains a short, current status summary and states that no restart, deployment, or configuration change was performed. The service name and response shown here are synthetic; the real environment stays out of the article.

The constraint came first
#

My homelab runs on Proxmox, Docker, GitOps, internal services, and a self-hosted agent platform. One of its agents understands the environment and can inspect its current state.

In a normal trusted session, that agent can do more than answer questions. It can work with operational tools behind the controls of the main platform.

That made the obvious integration unacceptable:

WhatsApp webhook -> public reverse proxy -> internal bot -> shell

It would make a good five-minute demo. It would also turn a compromised messaging account, bad webhook handler, or agent mistake into an infrastructure incident.

I started with harder requirements:

  • no publicly reachable listener or application route into the homelab for this integration;
  • accept messages from one configured WhatsApp sender identifier;
  • verify the webhook payload signature before processing anything;
  • keep infrastructure credentials inside the homelab;
  • withhold known deployment and change tools from this channel;
  • preserve that policy on the continuation and rebuild path I tested;
  • test representative forbidden operations and verify their postconditions.

Not “the agent should avoid writes.”

For this channel, the runtime must not provide those known deployment and change capabilities.

The missing inbound arrow
#

The finished path looks like this:

flowchart LR
    A["Ray-Ban Meta"] --> B["WhatsApp on phone"]
    B --> C["WhatsApp Business Platform"]
    C -->|"Webhook with payload signature"| D["Cloudflare Worker"]
    D --> E["Short-lived KV inbox"]
    F["Portless homelab agent"] -->|"Outbound HTTPS poll"| D
    F --> G["Self-hosted agent runtime"]
    G --> H["Exact inspect-capability allowlist"]
    F -->|"Outbound reply"| C

In this deployment, I did not configure a publicly reachable application route into the homelab for this integration. The homelab agent publishes no listener for it. Every connection from the homelab agent to the broker is initiated outbound over HTTPS; the agent retrieves one pending message, sends it into the internal runtime, and returns the answer through the messaging API.

Cloudflare is an airlock here, not a tunnel.

In this deployment, the public side receives the webhook and temporarily holds the message. I did not provision it with the credentials used by the homelab inspection tools. The credential used to send the final reply also stays inside the homelab.

That separation does not make the public component harmless. Message text is still data. The broker therefore has a deliberately small API, authenticated polling and acknowledgement, short retention, and no general route into anything behind it.

Verify the webhook before authorizing the sender identity
#

An allowlisted phone number is useful, but it is not enough on its own.

In my implementation, the Worker computes the expected HMAC over the raw body using the Meta app secret and compares it with the supplied signature before parsing the payload. This checks that the body matches a signature generated by a holder of that secret. It does not prove freshness, prevent replay of a previously valid delivery, or authenticate the human holding the phone or account. The Worker then accepts only the event type I expect, non-empty text, and one configured sender identifier as an authorization gate.

Conceptually, the gate looks like this:

const rawBody = await request.text();

if (!await validWebhookSignature(rawBody, suppliedSignature, appSecret)) {
  return new Response("Unauthorized", { status: 401 });
}

if (message.type !== "text" ||
    !message.text?.body?.trim() ||
    message.from !== allowedSenderId) {
  continue;
}

The broker endpoints used by the homelab agent have separate authentication. A legitimate webhook sender cannot automatically poll the inbox. An Internet client that discovers the Worker URL cannot retrieve pending messages without the broker credential.

Different trust boundary. Different credential.

Replay remains a separate control problem. This design does not claim that a valid delivery cannot be replayed, so repeated processing and disclosure remain possible failure modes.

Meta documents the WhatsApp Business Platform webhook and messaging flow, and publishes a signature-validation sample that illustrates the mechanism. I would not copy that sample verbatim as a production security control. The exact app roles and token controls are deliberately not reproduced here. Those interfaces change, and a point-in-time setup screen is not a security model.

Read-only is a capability, not a prompt
#

The first safe version gave the WhatsApp channel no tools at all.

That minimized the tool-mediated attack surface and was mostly useless. The agent could describe the stable architecture from memory, but it could not verify whether a container was running or whether a service was healthy right now.

The useful version exposes an exact allowlist of inspect-class capabilities. In the paths I tested, it can read selected documentation, inspect pull requests, query known infrastructure surfaces, and run guarded diagnostic commands.

It cannot see the tools used to:

  • create or update tasks and notes;
  • change documentation;
  • comment on, merge, or create pull requests;
  • deploy or restart services;
  • run unrestricted local or remote shell commands;
  • approve execution;
  • invoke built-in operational commands.

The runtime builds the normal tool registry and removes everything that is not explicitly allowed for this channel.

const enabledTools = filterToolsByAllowedNames(
  completeToolRegistry,
  WHATSAPP_READONLY_TOOL_NAMES,
);

This distinction matters.

A system prompt saying “never deploy” is guidance to a model. Withholding the known deployment tools from this channel is an enforcement decision made by the runtime.

Even the remaining inspection shell is not unrestricted shell with a reassuring name. In this implementation, it has command allowlists, denylists, protected-path controls, and secret-risk checks. During testing, one compound inspection command was blocked because the runtime could not prove the whole operation safe. The agent had to fall back to a narrower diagnostic command.

Tool-name filtering is only one layer. The allowed tools must themselves expose non-mutating operations, and their backing identities should be read-scoped to the minimum resource set where the platform supports it. Without independent evidence for every tool and credential path, the allowlist should not be read as proof that every possible mutation is impossible.

Defense in depth should occasionally say no to its owner.

The bug that mattered most
#

The main request path preserved the WhatsApp policy correctly.

The recovery path did not.

The agent platform uses durable jobs so an interrupted conversation can continue after a rebuild. In the first implementation, that continuation job did not carry the channel’s forced tool level, exact allowlist, and execution-approval state forward.

That created a subtle failure mode: a turn could begin under the constrained WhatsApp policy and, after interruption and recovery, continue under the chat’s normal capability level.

The fix was not another sentence in the prompt. The policy became part of the durable job state:

forced tool level
allowed tool names
execution approval state

On the continuation and rebuild path I tested, recovery restores those values instead of inferring them from mutable chat settings. The regression test interrupts a constrained turn and checks that its continuation receives the same policy state. I have not presented evidence here for every recovery entry point or for legacy, missing, malformed, or partially migrated policy records, so this result is deliberately scoped to that tested path.

This was the most important finding in the project.

A security boundary that exists only on the happy path is not a security boundary.

I tested what it must refuse
#

A successful health query proves that the integration works.

It does not prove that the boundary works.

The useful tests were the negative ones:

  • ask the agent to update a task;
  • ask it to deploy a service;
  • send an operational slash command as ordinary text;
  • attempt a command outside the inspection contract;
  • interrupt a request and recover it after a rebuild;
  • combine mismatched broker and agent protocol versions.

The expected result was not a polite refusal backed only by model behavior. In the representative paths I tested, the relevant capability had to be absent or independently blocked, followed by a postcondition check that the requested mutation had not occurred. These tests increase confidence in the exercised paths; they are not proof that every possible route is non-mutating.

I also designed the homelab agent to check a compatibility marker in the broker health response before consuming messages. In the mismatch case I tested, it stopped instead of guessing how the deployed Worker behaved. That catches the exercised protocol mismatch; it does not prove that every schema or behavior is compatible.

Version drift is an operational problem. At a trust boundary, it is also a security problem.

The broker is intentionally a single-user, low-concurrency design. Cloudflare documents that Workers KV is eventually consistent, so its KV-backed message index should not be mistaken for a transactional queue. Processing, reply delivery, and acknowledgement are separate failure domains. The agent retries unacknowledged messages, so duplicate processing or replies are possible. Because the index is not transactional, this design does not claim durable queue delivery semantics; expiration, stale reads, or index-update races can also delay or lose messages.

What read-only does not solve
#

Read-only is a narrower blast radius. It is not a synonym for safe.

An inspection tool can still reveal hostnames, internal topology, logs, repository content, or configuration details. A compromised messaging account could ask legitimate-looking questions and exfiltrate the answers without changing a single byte in the homelab. Sender allowlisting reduces who the Worker accepts; it does not provide an independent second factor when the phone or WhatsApp account is compromised.

The agent also reads data from systems that may contain untrusted text. Prompt injection does not stop being relevant because the channel cannot deploy. An attacker-controlled issue, document, log entry, or web page could still try to influence the response or extract information available to the model.

The current controls reduce those risks; they do not eliminate them:

  • one configured sender identifier is accepted;
  • the exposed tool set is narrow;
  • unrestricted shell is absent, while inspection tools apply command, protected-path, and secret-risk checks;
  • broker objects are configured with short TTLs, while the persistent agent chat has separate retention;
  • requests and replies transit Meta and Cloudflare and may be subject to their logging, retention, and account-access controls;
  • I treat anything returned to WhatsApp as disclosed to that messaging channel and avoid secrets, broad logs, and sensitive configuration output.

I would not use this channel for secrets, broad log retrieval, approval of writes, or incident response. Convenience is the reason it exists. Convenience is not permission to collapse trust boundaries.

Why I did not add WhatsApp approvals
#

The tempting next step is obvious:

Reply YES to deploy.

I did not build it.

The same account asking for a change would approve the change. The same conversation could carry the request, the confirmation, and the result. That is not meaningful separation of duties. It is one compromised channel saying yes to itself.

Writes belong in the trusted interface where the existing approval controls and operational context live.

The glasses can tell me what they found. They do not get to fix it.

The part worth reusing
#

Ray-Ban Meta makes the demo memorable. WhatsApp makes the interface convenient. Neither is the general lesson.

The reusable pattern is to treat every conversational channel as its own security principal. The same separation fits the GitOps approach I use elsewhere: make the boundary explicit, reviewable, and difficult to bypass accidentally.

  • authenticate it separately;
  • give it an explicit capability set;
  • keep credentials on the side that needs them;
  • make the recovery paths you support preserve the same policy and fail closed on missing policy state;
  • test representative forbidden operations, not only successful ones;
  • fail closed when components disagree about the protocol.

With WhatsApp connected, Ray-Ban documents that the glasses can send a message to a contact by voice, subject to regional, language, and feature availability. That is the only glasses-specific behavior this design needs.

“Read-only access” is shorthand. What I actually gave the glasses was a voice route into one constrained question-and-answer path.

That path can inspect selected state and return a live answer, while known deployment and change tools are withheld from its runtime.

Convenient enough to use while walking.

The glasses can tell me what the constrained path found. They do not get the deployment tools.