Skip to content
Back to skills

Lampos Sdk

BSecurity

Program a lampOS lamp from a developer's own computer with the lampos SDK (Python or JavaScript) or the lamp-mcp server. Use when someone asks to program the lamp, control a lampOS lamp, make the lamp react to something (presence, a build, a timer, an agent's progress, a message), write a lamp script or skill, pair a computer with a lamp, or test lamp code against the fake lamp.

  • 558 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added November 13, 2026
ai-agentsjavascriptpythongojavashellreactnodeexpresstestingdebugging

Works with

  • terminal
  • cli
  • api
  • mcp

Security analysis

B84/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro scans all 10 files and shows the line behind each finding

Scanned November 13, 2026

npx -y skills add autonomous-ai/lampOS --skill lampos-sdk --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Lampos Sdk?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Lampos Sdk
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/autonomous-ai-lampos-sdk/badge)](https://www.skillsdirectory.com/skills/autonomous-ai-lampos-sdk)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: lampos-sdk
description: Program a lampOS lamp from a developer's own computer with the lampos SDK (Python or JavaScript) or the lamp-mcp server. Use when someone asks to program the lamp, control a lampOS lamp, make the lamp react to something (presence, a build, a timer, an agent's progress, a message), write a lamp script or skill, pair a computer with a lamp, or test lamp code against the fake lamp.
---

# Programming a lampOS lamp

A lampOS lamp is a desk lamp with a body (head and arm), a light ring, a
voice, eyes and touch. A computer the owner paired with it can ask it to
move, light, speak and look, and hear what it notices, over one protocol
(`lamp-control`, WebSocket over TLS on port 7731). Three ways in, the same
protocol underneath:

| you write | use | install |
|---|---|---|
| Python | `lampos` | `pip install "git+https://github.com/autonomous-ai/lampOS@lamp-voice#subdirectory=lampOS/sdk/python"` |
| JavaScript (Node 20.11+) | `lampos` | `git clone --branch lamp-voice https://github.com/autonomous-ai/lampOS.git`, then in your project `npm install --install-links <the clone>/lampOS/sdk/js` (a copy, so Node finds its `ws`) |
| an AI agent's tools | `lamp-mcp` | both in one command: `pip install "lampos @ git+https://github.com/autonomous-ai/lampOS@lamp-voice#subdirectory=lampOS/sdk/python" "lamp-mcp @ git+https://github.com/autonomous-ai/lampOS@lamp-voice#subdirectory=lampOS/sdk/mcp"`, then `claude mcp add lamp -- lamp-mcp` (or any MCP client) |

The packages aren't on PyPI or npm yet (`pip install lampos` and
`npm install lampos` fail); install from the repository as above.
`lamp-mcp` needs `lampos` in the same `pip` command, or pip looks for it on
PyPI and fails.

**An MCP client usually starts `lamp-mcp` without your shell's environment**, so
`LAMPOS_HOME` is lost, the pairing isn't found, and the client shows only
"Connection closed". Give the folder `lamp pair` used: `--home PATH` in the
client's config (`claude mcp add lamp -- lamp-mcp --home ~/.config/lampos`), or
`python -m lamp_mcp` from the venv's python when the client can't find `lamp-mcp`
on its PATH. An unpaired start says in one stderr line which folder it looked in.

**On a customer lamp (stable 11450)** five things are missing: `play` and a
look at a face or name, a motion's end (`wait=True` returns when the lamp takes
it), limp and pose, mDNS (pair with `--host`), and local event times (`at` is
UTC). Skill push, `joints` for `roll`, `shoulder` and `elbow` (while the lamp
stands) and a head tap as a `touch` event are in 11450. Read
[references/gotchas.md](references/gotchas.md#on-a-customer-lamp-today-stable-11450)
before promising any of them.

## Start without a lamp

Always build and test against the **fake lamp** first: it speaks the real
protocol and answers as lamp-control does.

```sh
lamp fake --everything     # a fake lamp, paired with this computer; leave it running
python my_program.py       # in another terminal, same LAMPOS_HOME
```

`--everything` answers every op; without it the fake answers as a customer
lamp does: `look` and `joints` at yaw and pitch taken, `play` and `joints`
at roll, shoulder or elbow refused in lamp-control's words. It takes a skill
push, which a customer lamp refuses. In tests, use `lampos.testing.FakeLamp` (JS:
`FakeLamp` from `lampos/testing`): see [references/testing.md](references/testing.md).

## Pair with a real lamp

The owner makes a code in the lamp's app: an 8-digit code, the
certificate's pin (64 hex), and the grants this computer gets. Ask the
owner for the grants your program needs, by what it does:

| your program | grants |
|---|---|
| lights the ring | `light` |
| speaks or plays sounds | `speak` |
| moves (gestures, feelings, looks, moves) | `body` (a feeling needs `light` too) |
| knows who's there, follows a hand | `look` |
| hears a phrase or its name | `hear` |
| hears music, laughter, the door, footsteps | `sound` |
| puts skills of its own on the lamp | `skills` |
| reads the room's air, the lamp's own computer | `air`, `device` |
| sees pictures | `camera` |
| hears the room's sound (20 ms blocks of samples) | `microphone` |
| plays sound of its own through the speaker | `speaker` |

**A grant added later needs a new pairing code:** the owner can't widen a pairing. Then, on the computer:

```sh
lamp pair CODE --pin PIN --host lamp-xxxx.local --name my-laptop
lamp hello                 # connects, says this computer's grants
python scripts/check_pairing.py   # this skill's check: grants, limits, what each refusal means
```

The pairing (a key and the lamp's pin) is kept in `~/.config/lampos`, or
`LAMPOS_HOME`.

**A pairing holding a grant a lamp's build doesn't know** stops an older
lamp-control at its start (fixed from #4525 on). If you pair with a new
grant on a lamp that tries a newer build, revoke that pairing before the
lamp goes back to an older build.

## Reaching the lamp from off its network

By default the lamp answers only computers on its own network. Two ways from
elsewhere:

1. **The lamp's remote opt-in**, on a lamp whose lamp-control has it (`--remote`, or
   `LAMP_CONTROL_REMOTE=on`): off by default, and **the owner must switch it
   on** on the lamp. Then any address that reaches port 7731 is admitted
   (still only paired computers). Camera and microphone never leave the
   lamp's own network, whatever this says.
2. **An ssh tunnel to the lamp**, which needs the lamp's ssh: the owner's to
   give, and nothing else to switch on. `ssh -N -L 7731:127.0.0.1:7731
   orangepi@lamp-xxxx.local`, then `--host 127.0.0.1` for `lamp pair` and
   `Lamp.connect(host="127.0.0.1")`. Through a tunnel the lamp sees
   `127.0.0.1`, which is its own network (lamp-control counts loopback as
   local), so **a tunnel carries every grant, the camera and microphone
   included.** Never use or suggest a tunnel to get the camera or microphone
   off the lamp's network: that is the owner's decision when they give ssh,
   not a way around item 1.

## The mental model (read before writing code)

- **Grants:** the owner ticks what this computer may do: `body` (move),
  `light`, `speak`, `look` (presence, hands, face/hand targets), `hear`
  (phrases, the name), `sound` (sound classes), `skills` (push skills),
  `air` (the air sensor), `device` (the lamp's own computer), `camera`
  (pictures), `microphone` (the room's sound), `speaker` (sound of
  its own, through the lamp's speaker).
  A call without its grant is refused `no_grant`. Grants are read at hello.
- **Leases:** moving the body (`look`, `move`, `play`, `joints`) needs a
  `body` lease. Hold one with `async with lamp.lease("body"):` (JS
  `lamp.withLease(['body'], async (lease) => …)`). The lamp can take it back
  (a conversation, a hand on the head, private): you get a `lease` event
  with `state: lost` and the why. `gesture` and `express` need no lease.
- **Who wins:** the person always. A conversation, a touch, the app, private
  or paused mode come first, and the lamp tells you (`lease`, `lamp.changed`).
  Behind the person, one computer at a time holds the body.
- **Limits:** by default the rates are off: hello's `limits` says
  `"rates": "off"`, and only refused actions (20 a second, then the
  connection closes) and phrase changes (6 a minute) are bounded. A lamp run
  with `--rates default` sends a window for each kind. The body bounds every
  move whatever the rate. Read `lamp.limits` with `.get()`: an older lamp or
  fake leaves keys out.
- **Closing ends what you asked for:** when your program's last connection
  closes, the lamp gives back its light and stops its waiting lines at once.
  A script that lights the ring and exits shows it for a moment only: stay
  connected for as long as it should show.
- **A new motion cuts the one playing short:** a gesture, move or
  feeling's move asked while another plays replaces it, eased out from where
  the body is. While the person is using the lamp, the body drops the new
  one ("the person is using the lamp"). Stable 10824 and older refused it
  `busy` with `retry_ms` instead: wait and ask again.
- **Quiet hours:** a lamp setting (`limits.quiet_hours`, `off` by default,
  or a `{from, to}` window the lamp sets). `say` and `sound` are refused `quiet_hours` then; the
  light still works.
- **Be a living thing, not a machine:** react to something that happened, let
  a move finish, don't loop gestures. A lamp switching feelings every 3 s
  reads as twitchy.

## The calls

Every call, side by side in Python, JavaScript and MCP:
[references/api.md](references/api.md). The shape, in Python:

```python
import asyncio
from lampos import Lamp
from lampos.errors import LampError

async def main():
    async with await Lamp.connect() as lamp:          # the paired lamp
        await lamp.light("green", 3, 10_000, pattern="breathe")
        try:
            await lamp.say("The build passed.")
        except LampError as refused:                  # quiet_hours, private, busy...
            print(refused.code, refused.why)
        await lamp.on("person.arrived", lambda event: print("someone came", event))
        await lamp.wait_closed()

asyncio.run(main())
```

Every refusal is a `LampError` with `code` and `why`, the lamp's own words;
[references/errors.md](references/errors.md) has what to do about each code.

## Reading the lamp

- **Events** come by edge, when something changes: `person.arrived`,
  `person.left`, `person.facing`, `person.stood_up`, `hand`, `phrase.heard`,
  `name.heard`, `sound.heard`, `touch`, `button`, `lamp.changed`, `time`;
  `lease` and `done` come unasked. Subscribe with `lamp.on(event, handler)`.
- **State** with `lamp.get("presence", "light", "mood", "lamp")`; the
  body's joints, bounds and caps with `lamp.body_info()` (JS
  `lamp.bodyInfo()`, the `body` grant).
- What each carries, and its grant: [references/api.md](references/api.md#events-and-state).
- Never readable, by any grant but `camera` and `microphone`: pictures,
  audio. By any grant: transcripts, face prints, names. A hand's or face's
  place is a share of the picture.
- **The camera** (the `camera` grant): a snapshot, or a stream of
  frames; never while private, never off the lamp's own network:
  [references/camera.md](references/camera.md).
- **The microphone** (the `microphone` grant): Python
  `await lamp.microphone_stream(True)`, JS `lamp.microphoneStream(true)`,
  sends `microphone.audio` events, each a 20 ms block (16 kHz mono S16LE,
  640 bytes in `data`), until `microphone_stream(False)` /
  `microphoneStream(false)`, `stop`, or `microphone.ended`; off unless the
  owner switched it on, never while private or with someone else in view,
  never off the lamp's own network:
  [references/api.md](references/api.md#the-microphone).
- **The speaker** (the `speaker` grant, never ticked for you): Python
  `async with lamp.speaker_stream() as out: await out.speaker_write(pcm)`
  (plain call `speaker_stream_request`), JS `lamp.speakerStream()` with
  `speakerWrite`, the command `lamp speaker FILE.wav` and the MCP tool
  `lamp_play_audio` play sound of your own through the lamp's speaker
  (mono S16LE, 16000 or 24000 Hz); off unless the owner switched it on,
  never while private or muted, never off the lamp's own network:
  [references/api.md](references/api.md#the-speaker). `body.state` and
  routed `joints` come next; this skill gains them as they land.

## Patterns

Ready to adapt, each in Python and JS: react to presence, a CI light, a
pomodoro, an agent's progress shown on the lamp, a chat alert, a program
that runs for days, and a web app on the SDK:
[references/patterns.md](references/patterns.md).

## Gotchas we hit

Read [references/gotchas.md](references/gotchas.md) before your first real
run: what a customer lamp lacks today, breathe and pulse levels, one pulse a
minute, `busy` while a motion plays, the light's `shows`, the restart mute.

## Debugging

`LAMPOS_LOG=debug` prints every frame to and from the lamp on stderr, secrets
redacted. The lamp's side and the journal recipe:
[references/debugging.md](references/debugging.md).

Files in this skill

  • SKILL.md11.8 KB
  • references/api.md20.3 KB
  • references/camera.md2.8 KB
  • references/debugging.md2.8 KB
  • references/errors.md2.7 KB
  • references/gotchas.md8.4 KB
  • references/patterns.md10.4 KB
  • references/testing.md5.6 KB
  • scripts/check_pairing.py2.2 KB
  • scripts/try_fake.sh1.2 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in ai-agents →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.