The owner says "I want to talk to you from my phone" or "set up the Passport". You drive it from start to finish: the ambient channel runs on this host, reaches the owner's cerebellum, serves one pocket room to the owner's tailnet over HTTPS, and the owner's 多多随身 app and Passport talk to it. The owner only supplies the cerebellum token on the host terminal, approves each change, and does the taps on the phone and the Passport.
What exists before you start, and is not this skill's work:
| Piece | Where it comes from |
|---|---|
A cerebellum the owner can reach: wss:// URL and token |
The owner runs one, or someone runs it for them. Self-hosting it and its model services: openduo/ambient, docs/requirements.md, docs/deploy.md, services/ |
| The 多多随身 app on the owner's iPhone | TestFlight beta: apply with the sign-up form; the invitation arrives by email. Source: openduo/pocket-ios |
| A FoloToy AI Passport with pocket firmware (optional) | openduo/pocket-passport, docs/pocket/README.md |
| Tailscale on this host and an account for the phone | The owner's own tailnet |
General policy
Follow these throughout. Each rule carries its reason.
- Tailnet only, never public. The page and API are reached through
tailscale serve(or an SSH tunnel for one machine), nevertailscale funnelor any other public ingress. Reason: the channel has no login of its own; tailnet membership is its only access control, so a public address hands the room, its history and its microphone path to anyone. - Never change something that already exists to make room: another service's serve route, the tailnet policy, a firewall, another channel's room. Adding is allowed; moving, replacing or deleting is not. If the only way needs a change, stop and give the owner the options. Reason: those belong to work you cannot see.
- The token never passes through chat. The owner writes
AMBIENT_CEREBELLUM_TOKENinto~/.config/duoduo/.envon the host terminal (step 2). You check that it is set, never what it is, and never print, echo, log or paste it. Reason: every reply lands in duoduo's event log, and a leaked token lets anyone use the owner's cerebellum. - Each change needs the owner's yes, one at a time: each install,
.envedit, room creation, start or restart, andtailscale servechange. Reads and checks need no approval. Reason: a restart severs every connected device, and a serve route changes who reaches this host. - One channel per room, one phone per room. Never start a second channel (on this host or
another) for a room that is already served. Reason: a second link to the cerebellum for the same
room takes it over, and the first channel stops for good (
superseded). - Prove it, hand it over, say what is proven. A step is done when its check passed. Transport health is not acoustic acceptance: say which of the phone, voice-note and Passport checks the owner actually did. Reason: the owner decides on risk.
Set it up, start to finish
0. Prerequisites
Ask or check, then stop at the first gap and point the owner to its row in the table above.
duoduo daemon status # the channel needs the running daemon
duoduo channel list # is ambient already installed?
tailscale status --self # this host on a tailnet; note <node>.<tailnet>.ts.net
tailscale serve status # what is already served (policy 2)
On macOS, tailscale is the app's CLI by full path if it is not on PATH. Ask the owner:
- "Do you have your cerebellum's
wss://address and its token?" Only the URL may be said in chat. - "Did someone who runs a cerebellum for you send a share link or an onboarding text?" If so, read references/shared-cerebellum.md first: it covers accepting the shared node and getting the token without it passing through chat.
- "Is 多多随身 installed on your iPhone, and is your Passport flashed with pocket firmware?" If the app is not installed, give the owner the TestFlight sign-up form and continue with the host steps while the invitation is pending.
- "Which Tailscale account will the phone log in with?" It must be the same tailnet as this host.
If the channel is installed and a room is served already, this is not a setup: go to references/operations.md.
1. Install the channel
duoduo channel install @openduo/channel-ambient
duoduo channel list # shows ambient and its version
The install also copies the kind config (config/ambient.md) into the daemon's kernel config
directory when none is there. Do not edit its bridge: block: every key is required and the
shipped values carry their basis. Verbs, endpoints and logs: references/channel.md.
2. The channel's environment
The duoduo CLI loads ~/.config/duoduo/.env when it starts a channel, filling only keys not
already set in its environment, and passes the plugin only the keys on its allowlist. Write the
keys there, not under the plugin directory.
-
Pick a free loopback port:
lsof -nP -iTCP:<port> -sTCP:LISTENprints nothing. There is no default on purpose. -
With the owner's yes, add
AMBIENT_HTTP_PORT=<port>andAMBIENT_CEREBELLUM_URL=wss://…(the URL the owner gave). -
The owner adds the token on the host terminal. Send them this, verbatim:
bash -c 'read -rsp "Cerebellum token: " T; echo; printf "AMBIENT_CEREBELLUM_TOKEN=%s\n" "$T" >> ~/.config/duoduo/.env' chmod 600 ~/.config/duoduo/.env -
Check without reading the value:
grep -cE '^AMBIENT_(HTTP_PORT|CEREBELLUM_URL)=.' ~/.config/duoduo/.env # 2 grep -qE '^AMBIENT_CEREBELLUM_TOKEN=.' ~/.config/duoduo/.env && echo token setIf a key appears twice, ask the owner which line stays. Full key table: references/channel.md, Environment.
3. Create the pocket room
Agree with the owner: a room id ([A-Za-z0-9_-]+, for example my-phone), the absolute workspace
the room's agent works in, and the runtime (claude, codex, grok or pi). None has a default.
One room per phone.
duoduo channel ambient room add <room_id> --workspace <absolute path> --runtime <runtime> \
--name "<display name>" --pocket
duoduo channel ambient room list
--pocket writes the pocket room's instance prompt: short answers, conclusion first, no markdown,
because answers are read on the phone and the latest one on the Passport's 240×320 screen. The
verb refuses a room that exists.
Every published channel (0.1.0 and later) has the verb. If room is an unknown verb (a channel
installed from an older source build), use the fallback script, with the same arguments and the
same approval:
<this skill>/scripts/create-room.sh <room_id> <absolute path> <runtime> --pocket
It calls the daemon's channel.spawn over its Unix socket, refuses an existing room, and writes the
pocket note into the room's descriptor.md when that body is empty. The read-only TCP port
(127.0.0.1:20233) refuses channel.spawn; do not try it. Without the verb there is no
--name; the page shows the room id.
4. Publish to the tailnet
The channel binds 127.0.0.1 only and its host gate admits loopback and private LAN addresses,
not tailnet 100.64.0.0/10 addresses or *.ts.net names. So tailscale serve carries HTTPS to
loopback, and the channel is told the name.
-
Read
tailscale serve status. If:443on this node is free, with the owner's yes:tailscale serve --bg --https=443 http://127.0.0.1:<port> tailscale serve status # the new entry, every earlier entry unchangedIf
:443already serves something else, do not touch it. Give the owner the options in references/operations.md, Serve port taken. Ifserveasks for a consent URL (HTTPS certificates off for the tailnet), hand it to the owner as one message and wait for "done". -
With the owner's yes, add to
~/.config/duoduo/.env:AMBIENT_HTTP_HOSTS=<node>.<tailnet>.ts.net AMBIENT_HTTP_ORIGINS=https://<node>.<tailnet>.ts.net
5. Start and accept the host side
With the owner's yes: duoduo channel ambient start (or stop, then start, if it was running).
duoduo channel ambient status
curl -s http://127.0.0.1:<port>/healthz # {"ok":true,"rooms":<n>}
curl -s "http://127.0.0.1:<port>/api/state?room=<room_id>"
Pass: daemon_ok and cerebellum_ok are true, config_issues is empty. Then from another
tailnet device (not this host): curl -s https://<node>.<tailnet>.ts.net/healthz answers the
same JSON. A 403 {"error":"host not allowed"} means step 4.2 is missing or the channel was not
restarted. Anything else: references/channel.md, Troubleshooting.
6. Connect the phone
Give the owner the values, then walk them through references/pocket-app.md:
| App field (zh / en) | Value |
|---|---|
| Channel Host / Channel Host | <node>.<tailnet>.ts.net |
| Port · HTTPS / Port · HTTPS | 443, HTTPS on (the defaults) |
| Room name / Room name | <room_id> |
The app logs in to Tailscale itself (it appears as node duoduo-pocket), then Check Connection / Check
Connection runs Tailscale → /healthz → /api/state and must pass all three.
Acceptance, done by the owner and confirmed to you:
- Type a message in the app; an answer from 多多 arrives.
- Hold to talk (Hold to Talk / Hold to Talk), say a sentence, release; the transcript appears and an answer follows.
- Optional: turn on ambient mode (the waveform button, top right); answers are spoken only while it is on, otherwise shown.
Check each in the room: curl -s "http://127.0.0.1:<port>/api/state?room=<room_id>" shows
ws_clients at least 1 while the app is open.
7. Pair and use the Passport
Walk the owner through references/passport.md: Add Passport / Add Passport in the app, numeric comparison confirmed with OK on the Passport and 配对 on the iPhone, then hold OK, speak, release. Pass: the transcript shows on the Passport, then the answer, and the same exchange appears in the app.
8. Write the handoff
Write a short note beside the channel's state (ask the owner where, for example
~/.config/duoduo/ambient-HANDOFF.md). No credentials in it. Record: purpose; channel version;
AMBIENT_HTTP_PORT; the cerebellum URL and which file holds the token (by path, never the value);
each room with workspace, runtime and display name; the serve entry (https://<node>… →
http://127.0.0.1:<port>) and every serve entry that existed before; the app's host and room;
whether a Passport is paired; which acceptance checks passed and which the owner did not do;
start, stop, verify and rollback (references/operations.md); do not
touch: other serve routes, the tailnet policy, other channels.
Later
- Restart, upgrade, add a room, stop, roll back, a serve port that is taken, moving the channel to another host: references/operations.md.
- Something does not work (401, superseded, host not allowed, a room that will not start): references/channel.md, Troubleshooting.
- The owner wants only the phone to reach the channel, not every tailnet device: references/tailnet-acl.md. You draft; the owner applies.
- App screens, banners and what they mean: references/pocket-app.md. Passport controls, settings, re-pairing: references/passport.md.