lamps — the docs
A lamp is a folder with a ceiling: a manifest (lamp.json) that declares what the program may touch, and a program (lamp.syn) that does one job. You run it from the terminal; your agent gets it as a tool; the runtime enforces the ceiling. This page is everything, in order: install, pull, run, update, create, publish, the rules.
1 · Install the CLI
curl -fsSL https://lamps.sh/install | sh # macOS, Linux
irm https://lamps.sh/install.ps1 | iex # Windows
The script detects your OS, downloads the lamp binary from the GitHub release, verifies its sha256, puts it in ~/.lamps/bin and adds that to your PATH. It is forty lines; read it first. lamp upgrade checks for a newer release and tells you how to get it.
2 · Pull (download a lamp)
lamp pull git official set (github.com/synsema/lamps)
lamp pull owner/repo one lamp per repo: lamp.json at the repo root
lamp pull owner/repo@v1.2.0 a tag; without @, the latest tag (main if none)
lamp pull owner/repo/name MANY lamps per repo: a folder per lamp
lamp pull github.com/owner/repo full URL, same thing
lamp pull ./my-lamp a local folder
lamp add <ref> alias of pull
pull fetches the files the manifest lists, one by one, straight from the git host — no git, no archive, no install step, nothing executed. Each file gets a sha256 recorded in .pulled next to the lamp. Then the promise linter runs: if the source asks for anything the manifest never declared, you see BREAKS ITS PROMISE with each line, before anything can run.
lamp list
lamp inspect <lamp>
3 · Run
lamp run git log '{"n": 5}'
lamp git log '{"n": 5}'
LAMP_TIMEOUT=60 lamp run ...
Every run is a child process under the effective ceiling — manifest ∩ project ∩ session. The lamp prints one JSON value. A refusal by the lamp's own policy comes back as data; a refusal by the runtime comes back as structured DENIED entries with the reason. That is the system working — never retry with a wider ceiling. Every check, granted or denied, lands in lamp audit.
4 · Update
lamp update <ref>
update is pull of the latest tag. Hashes decide: if no file changed, nothing is written and nothing is counted twice on the hub — a machine counts once per lamp per day, whatever it does.
5 · Create a lamp
A lamp is one folder with two files. Make it, check it, run it locally — no registration, no account, no tooling beyond the CLI and the Synsema runtime.
my-lamp/
├─ lamp.json the manifest
├─ lamp.syn the program (Synsema)
└─ README.md
{
"name": "my-lamp",
"version": "0.1.0",
"description": "One line: what it does and what it refuses.",
"profile": "pure",
"caps": "stdout,env=LAMP_*,net=api.example.com",
"files": ["lamp.syn"],
"tools": [
{ "name": "fetch",
"description": "What an agent reads to decide.",
"parameters": { "type": "object", "properties": { "id": { "type": "integer" } } } }
]
}
- caps — the ceiling, in --cap-set syntax: family or family=scope, comma-separated. stdout · env=LAMP_* · net=host · file.read={root}/* · exec=git · db={root}/* · secret=APP_*. Ask precisely: the runtime denies, it does not trim.
- profile — pure: no filesystem, no processes, no database drivers exist at runtime (two walls). native: they exist; the ceiling is the wall. exec requires native.
- name — MUST equal the repo name, or the folder name in a multi-lamp repo. Identity is the address, never what the json claims.
- tools — name, description, JSON Schema parameters. Exactly what an MCP client receives and what a human types.
- files — what pull fetches and hashes. Paths inside the folder only.
- {root} and {dir} — substituted at run time in caps and source: the project directory and the lamp's own folder.
The program reads four environment variables and prints one JSON value:
LAMP_TOOL "fetch"
LAMP_ARGS {"id": 5}
LAMP_ROOT the project directory
LAMP_DIR the lamp's own folder
intent: "one line, frozen at startup"
require env("LAMP_*")
require net("api.example.com")
task main()
let a be json_decode(env("LAMP_ARGS", "{}"))
let r be http_get("https://api.example.com/item/" + text(floor(number(a["id"]))))
print(json_encode({"ok": r["ok"], "status": r["status"]}))
when env("LAMP_TOOL", "") != ""
main()
test "ids are numbers"
assert_eq(floor(number("5")), 5)
Check it before anyone pulls it:
synsema test lamp.syn
lamp inspect ./my-lamp
lamp run ./my-lamp fetch '{"id": 5}'
6 · Publish (appear on the hub)
Publishing is pushing to a public repo. There is no account and no login — your git host identity is your identity. Identity is the address, GitHub-style: nobody can publish under vercel/… without controlling github.com/vercel, and the manifest's name must equal the repo name (or the folder name in a multi-lamp repo) — a mismatch is refused at pull and never listed. The hub lists a lamp automatically the first time anyone pulls it: it fetches the manifest and the program, validates them, runs the promise linter, and puts it in the catalog with honest labels — community, and BREAKS ITS PROMISE when the source asks above its declared ceiling.
# one lamp per repo: lamp.json at the root, name == repo
git push
lamp publish owner/repo
# many lamps per repo: a folder per lamp, name == folder
lamp pull owner/repo/name
# the badge for your README

Identity on the hub is owner/name, first-come per name and always shown with its owner — the owner in front of every name is the trust signal. Versions are git tags; without tags the default branch is used. The official namespace (lamp pull git) is the curated monorepo; everything else is the open ecosystem.
7 · The rules
- A manifest MUST declare caps. No caps → it does not load and it is never listed.
- The manifest's name MUST equal the repo name (or the folder name in a monorepo). Identity is where a lamp lives, never what its json claims.
- exec MUST name its binaries (exec=git,exec=ls). Bare exec or exec=* never loads and is never listed — the hole was never execution, it was execution with nothing bounding it.
- exec requires profile native; the pure profile has no processes to run.
- What runs is always effective = require ∩ manifest ∩ project ∩ session. Each layer can only narrow. Under a ceiling the runtime denies, it does not trim.
- pull fetches only the files the manifest lists; a path that escapes the folder is refused. No archives, no postinstall scripts, nothing executed on pull.
- The program prints exactly one JSON value. Refusals are data, on the record, in the audit.
- Enabling a lamp for agents is a human act — editing a file or committing a folder. It is never a tool an agent can call.
- The linter is a lint, not a proof: it compares the text before you enable; the runtime is what decides at the call. Both are on your side.
8 · For agents (MCP + skill)
lamp mcp
Every enabled lamp's tools appear as lamp_<name>_<tool> with the effective ceiling stated in each description, so the model knows its own limits. lamps_list and lamps_audit are always there; lamps_eval exists only when a human wrote ~/.lamps/session.json with "eval": true. It starts from the working directory, so the tools change with the project and the agent's configuration does not. A lamp that is installed but not enabled is invisible to the model.
lamp skill # writes ./.agents/skills: the lamps skill, plus one SKILL.md per lamp you pulled
9 · Where things live, and the ceilings you set
<project>/.lamps/<name>/ committing IS enabling
<project>/.lamps/<name>/policy.json config-only for a global lamp
<project>/.lamps/config.json the project ceiling
~/.lamps/lamps/<owner>/<name>/ pulled lamps
~/.lamps/enabled.json which globals agents may see
~/.lamps/session.json the ceiling over everything
~/.lamps/audit/log.jsonl asked · granted · denied
Precedence: project over user, like every tool with a project config. lamp init writes a starting ./.lamps/config.json; tighten it and commit it — a tighter ceiling costs fewer prompts, not more.
10 · Trust, honestly
- Under the pure profile there is nothing to escape to: no filesystem, no processes, no drivers exist. Two walls.
- Under the native profile the ceiling is the only wall and the lamp is an ordinary OS process. A runtime bug is a sandbox bug. Prefer pure when you can.
- The hub does not gatekeep the open ecosystem; it labels it. Read the ceiling and the promise before you enable anything.
- Signing and content addressing are on the roadmap; today pull records a sha256 per file and update notices any change.