unlock_door tool identifies the guest by the phone number WhatsApp verified, looks for an active booking in Data, applies a per-guest rate limit, pulses a relay through a small HTTP API on a Raspberry Pi, and writes an audit entry. A staff-only tool registers bookings. It is five files, four TypeScript and one Python, and every TypeScript file compiles against lua-cli 3.33.0; run it with the steps on Running any example.
Verified against lua-cli 3.33.0.
The conversation
- A guest writes “open the front door”. The model calls
unlock_doorwithdoor: "front"; the tool reads the guest’s phone numbers from the profile, finds a booking that is active now and includes that door, checks that the guest has fewer than three unlocks in the last minute, calls the Pi, and logs the unlock. - A guest with no booking gets a refusal from the tool, and the persona tells them to contact the front desk. The model can’t bypass the check, because the check lives inside the only tool that unlocks.
- At check-in, a staff member calls
register_guest. The tool’sconditionhides it from everyone whose profile isn’t markedrole: 'staff'.
Primitives and channels
- Skill and tools:
door-access, withunlock_doorandregister_guest. - Runtime objects:
User.getand_luaProfile.mobileNumbers,Data.get,Data.create, andenvforDOOR_CONTROLLER_URL,DOOR_CONTROLLER_KEY, andUNLOCK_MS. - Channels: WhatsApp. The verified phone number is the whole identity model, so on a channel without one the tool refuses.
- Hardware: a Raspberry Pi with a relay module on BCM pins 17 (front) and 27 (garage), driving a fail-secure electric strike from its own 12 V supply.
The code
The Pi accepts only the door names it knows and caps the pulse length; the agent never sends a pin number.edge_api.py
src/skills/tools/UnlockDoorTool.ts
condition unless the profile says role: 'staff'; a hidden tool is not in the model’s tool list at all.
src/skills/tools/RegisterGuestTool.ts
context tells the model that a refusal is final.
src/skills/door-access.skill.ts
src/index.ts
First run
On the Pi (Raspberry Pi OS), install the dependencies and start the API with a secret of your own.lua test, User.get() resolves to your own developer profile; with no channel-verified phone number on it, the tool refuses before it touches the Pi, which is what it does on any channel without a verified number.
Output
DOOR_CONTROLLER_URL at an address the platform can reach (see Ways to make it yours), and set the variables with lua env production. Release it with lua push all --ci --force, lua version create --ci -m "<message>", and lua version promote <n> (Release an agent to production explains what each command changes), register your own number with register_guest from a staff conversation, and send “open the front door” from that phone.
Ways to make it yours
- Deployed code runs on the platform, so
DOOR_CONTROLLER_URLmust be an HTTPS address the platform can reach, such as a reverse proxy or tunnel in front of the Pi;raspberrypi.localworks only forlua teston your own network. To avoid an inbound port, connect the Pi as a device instead, so it dials out and its commands become tools. - Mark staff by writing
role: 'staff'to their profile from a tool or the user data REST endpoints;conditionhidesregister_guestfrom everyone else. - Create bookings from your property system with a webhook instead of
register_guest, writing the same fields toguests. - Tune the limits: three unlocks per minute in
unlock_door, a two-second throttle and a 10-second cap inedge_api.py, andUNLOCK_MSper building.
Next steps
Identify users
What the profile holds per channel and how to add fields such as
role.Connect your first device
Let hardware dial out to the platform instead of exposing an HTTP port.
Store and search data
Filters, pagination, and indexes for collections such as
guests.
