AWS Builder Center
Using AWS IoT Core to Deliver AI Actions to a Mac

Using AWS IoT Core to Deliver AI Actions to a Mac

I used AWS IoT Core Device Shadow to bridge a cloud AI agent and a Mac, so approved Homebrew installs and model downloads run locally—even after reconnecting.

Series: AWS IoT Core (2 articles)

  1. 2
    Using AWS IoT Core to Deliver AI Actions to a Mac This article
Cloud Engineer
Homebrew is the package manager many Mac users reach for when they need a command-line tool. Hugging Face is where the machine learning community publishes models and datasets. Both are useful, but both start with the same assumption: you already know what to look for.
I wanted to remove that assumption. I built an application where someone can ask in plain language for “a lightweight image viewer” or “a small Japanese summarization model.” An AI agent searches Homebrew and Hugging Face, presents candidates, and waits for an explicit confirmation. Only then does it install a package or download a model.
The install step led to the most interesting architectural decision in the project: I used AWS IoT Core to send approved work from AWS to a user's Mac.

The problem: the agent is in AWS, but the work is on the Mac

The agent runs with Strands Agents  on Amazon Bedrock AgentCore Runtime . AgentCore Runtime is a serverless environment designed for hosting agents and tools, and it works with frameworks such as Strands.
The agent needs tools to search Homebrew's public formula list and the Hugging Face Hub API. I put those searches behind one AWS Lambda function, then exposed them through Amazon Bedrock AgentCore Gateway . Gateway can expose Lambda functions as tools, which gives the agent a controlled route to call the search backend.
That solves discovery. Installation is different.
brew install has to run on the user's Mac, not in a Lambda function. The same is true for downloading a model into local storage. AWS must not simply reach into a laptop and execute arbitrary shell commands.
I needed a bridge with a clear contract:
  1. The cloud application records an approved request.
  2. A small process on the Mac receives it and performs a limited local action.
  3. The Mac reports the result back to the cloud.

The unexpected bridge: AWS IoT Core

AWS IoT Core is commonly associated with sensors, smart appliances, and industrial devices. It provides secure bidirectional communication between connected devices and AWS services.
For this project, I registered the Mac itself as the device.
That makes the laptop a well-defined endpoint with its own identity and permissions, rather than a machine that exposes a general-purpose remote shell. The Mac connects to AWS IoT Core using MQTT over TLS, and a local background agent is responsible for receiving and validating requests.
The key feature is AWS IoT Device Shadow .

Device Shadow is a small shared state document

A Device Shadow keeps a JSON document that represents a device's state. Its two most useful sections for this project are:
  • desired: what an application wants the device to do
  • reported: what the device says it has done
The cloud application writes an approved installation request into desired.
1
2
3
4
5
6
7
8
9
{
"state": {
"desired": {
"requestId": "install-node-001",
"source": "homebrew",
"id": "node"
}
}
}
The Mac agent receives the difference between desired and reported, validates the request, runs the approved operation, and writes its outcome to reported.
1
2
3
4
5
6
7
8
9
{
"state": {
"reported": {
"requestId": "install-node-001",
"status": "succeeded",
"completedAt": "2026-09-16T08:46:05Z"
}
}
}
AWS IoT calls the difference between the two sections a delta. This lets the Mac focus on pending work instead of reprocessing the full document. AWS documents recommend that cloud applications write requested changes to desired, while devices write their actual state to reported. The Shadow documentation  also describes versioning and timestamps that are useful for correlating requests and results.

Writing the request is deliberately small

The Lambda function does not run an installation. It only writes a request to the Thing Shadow and returns an acknowledgement.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
shadow_update = {
"state": {
"desired": {
"requestId": request_id,
"source": source,
"id": package_id,
}
}
}

iot_data.update_thing_shadow(
thingName=thing_name,
payload=json.dumps(shadow_update).encode("utf-8"),
)

return {"status": "accepted", "request_id": request_id}
This boundary is intentional. “Accepted” means the request was recorded for the Mac; it does not claim that the install has already completed. Completion only becomes visible when the Mac writes its result to reported.

Handling an offline laptop

This state-based design has a useful side effect: the Mac does not have to be online at the moment the request is created.
When the laptop reconnects, the local agent retrieves its Shadow and reconciles any pending desired state. While it is connected, the agent can also subscribe to Shadow delta updates for near-real-time handling. The Shadow service stores state independently of the device connection, allowing cloud services and devices to reconnect without losing the current state. See Retaining device state while the device is offline with Device Shadows .
In practice, the local agent follows this sequence:
  1. Connect with its device certificate.
  2. Subscribe to Shadow delta updates.
  3. Retrieve the current Shadow on startup or reconnect.
  4. Validate and execute each pending request exactly once.
  5. Write success or failure to reported.
Retrieving the Shadow on reconnect matters. A delta subscription is useful for new work, but startup reconciliation is what makes the design resilient when the Mac was asleep or offline.

Do not turn this into remote shell access

The most important design rule is that the cloud does not send arbitrary shell text for the Mac to execute.
This is unsafe:
1
subprocess.run(command_from_cloud, shell=True)
Instead, the request uses a small command vocabulary, and the Mac enforces an allowlist.
1
2
3
4
5
6
7
ALLOWED_PACKAGES = {"node", "python", "gh"}

if request["source"] == "homebrew":
package = request["id"]
if package not in ALLOWED_PACKAGES:
raise ValueError("Package is not approved")
subprocess.run(["brew", "install", package], check=True)
The same principle applies to Hugging Face model downloads: allow known actions, validate identifiers, set resource limits, and report failures explicitly. User confirmation happens before the request reaches the Shadow; the Mac then executes only the narrow operation it has been designed to accept.
At the AWS IoT layer, each Mac should have a unique X.509 certificate and an IoT policy restricted to that Thing's own Shadow topics. AWS recommends unique client certificates so that a single device can be managed or revoked independently. See X.509 client certificates .

The rest of the application

The project also includes a chat interface and multilingual responses in Japanese, English, Chinese, French, German, Spanish, Portuguese, Arabic, and Thai.
One implementation detail mattered here: the agent instance is cached per conversation session. If I put the selected language into the agent's fixed system prompt, the first language could become sticky for the session. Instead, I prepend a language instruction to every request. Package names and model IDs stay untouched, while the response itself follows the selected language.
For Arabic, translation was not enough. The UI also needed genuine right-to-left layout support, including the components that did not automatically mirror themselves.
I used AWS Labs' nx-plugin-for-aws to scaffold much of the project: the agent, website, authentication, and supporting infrastructure. It accelerated the boilerplate, but the chat experience and this local-execution bridge still required application-specific design.

Why this was the most fun part

Using AWS IoT Core for a laptop is not the obvious choice. It is a service designed for connected devices, and a Mac is usually thought of as a client computer rather than an IoT Thing.
But the fit is surprisingly good when the problem is “record an approved desired action in the cloud, let a remote endpoint perform it when available, and receive its reported result.”
The resulting pattern is useful beyond package installation:
  • run a tightly scoped local CLI task after an approved cloud workflow
  • fetch a local model after a user confirms a recommendation
  • coordinate a kiosk or demonstration Mac
  • notify a local development machine that cloud work is ready for a final step
The important distinction is between an instruction protocol and remote shell access. AWS IoT Core plus Device Shadow gave this project a durable, auditable state handoff without granting the cloud unrestricted control of the laptop.
Using a service normally associated with smart light bulbs to message a Mac turned out to be the most unexpectedly useful part of the system.

Series: AWS IoT Core (2 articles)

  1. 2
    Using AWS IoT Core to Deliver AI Actions to a Mac This article
Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article