{"id":"add-dial-tool","name":"add-dial-tool","summary":"選ばれたNanoClawエージェントにコンテナツールとして実際の電話番号を与えましょう。エージェントイメージに組み込まれた「ダイヤル」CLIと、エージェントごとにスコープ化されたOneCLI api.getdial.ai 認証注入を組み合わせ、選択したエージェントがSMSを送信したり、AI音声通話をかけたり、サンドボ…","body":"# Add Dial Tool\n\nInstalls Dial as a **container tool**: the `dial` CLI on the agent's `PATH`, the\n`dial-cli` skill so the agent knows how to drive it, and an OneCLI credential so\nin-container calls are injected keyless. Independent of the Dial **channel**\n(`/add-dial`) — install this alone. Idempotent: re-run it to change which agents\nmay use Dial.\n\n**This tool spends money and reaches real people.** An agent with Dial access can\ntext and call any number and buy more numbers, billed to the Dial account. The\nCLI and the skill file land in every agent's container, but the **key** is\ninjected per agent by OneCLI, so the operator chooses which agents get it. Every\nother agent gets an OneCLI block rule and sees `403 blocked_by_policy` if it\ntries.\n\nRun this from the NanoClaw repo on the host (not from a chat with an agent — the\ncontainer can't install itself). The mechanical steps carry `nc:` directive\nfences: an agent reads the prose and applies them, and a parser can apply them\ndeterministically from the same document. Every directive is idempotent, so the\nwhole skill is safe to re-run; anything a parser can't apply falls back to the\nprose beside it.\n\n## Pre-flight\n\nOneCLI is required for credential injection — without it there is no way to hand\nthe key to a container without putting it in an env var. This must succeed before\nanything else runs:\n\n```nc:run effect:check\ncommand -v onecli >/dev/null\n```\n\nIf it fails, tell the user to run `/init-onecli` first, then retry. Stop here.\n\nCalls this setup makes to Dial identify the install. The `dial` CLI prepends\n`DIAL_USER_AGENT` to its own token, so the account's requests stay attributable\nto this NanoClaw install in Dial's server-side logs. Resolve the token once\n(`nanoclaw/<version>`; an unreadable `package.json` degrades to\n`nanoclaw/unknown` rather than blocking the install):\n\n```nc:run capture:dial_ua validate:^nanoclaw/\\S+$ effect:fetch\nnode -p \"'nanoclaw/'+(require('./package.json').version||'unknown')\" 2>/dev/null || echo nanoclaw/unknown\n```\n\nPrefix every `dial` command below with `DIAL_USER_AGENT={{dial_ua}}`.\n\n## Choose which agents may use Dial\n\nList the agent groups (the NanoClaw service must be running — `ncl` talks to it\nover its socket):\n\n```nc:run capture:agent_groups effect:fetch\nncl groups list --json | jq -r 'if (.data|length)==0 then \"no agent groups yet\" else [.data[] | \"\\(.id) (\\(.name))\"] | join(\", \") end'\n```\n\nAsk the operator which of them may use Dial. Say plainly what they are granting,\nand ask even when there is a single agent:\n\n```nc:operator\nAgents on this install: {{agent_groups}}. Giving an agent Dial lets it text and call any number and buy numbers, billed to your Dial account. Agents you leave out are blocked at the gateway (reversible by running /add-dial-tool again). Agents created after this run have Dial until the next run.\n```\n```nc:prompt dial_agents validate:^(all|none|ag-[A-Za-z0-9-]+(\\s*,\\s*ag-[A-Za-z0-9-]+)*)$ normalize:trim\nWhich agents may use Dial? Enter agent ids separated by commas (the `ag-…` column), `all` for every agent, or `none` to install the tool with every agent blocked for now.\n```\n\n`all` and `none` cannot be mixed with ids, and an empty answer is never\n\"everyone\". A typo must not silently open or close anything, so every id named\nmust be a real agent group:\n\n```nc:run effect:check\nfor w in $(printf '%s' '{{dial_agents}}' | tr ',' ' '); do case \"$w\" in all|none) ;; *) ncl groups list --json | jq -e --arg id \"$w\" '.data[] | select(.id==$id)' >/dev/null || { echo \"unknown agent group '$w' — see: ncl groups list\" >&2; exit 1; }; esac; done\n```\n\n## Install the Dial CLI on the host\n\nThe host needs the `dial` CLI to sign in: `dial auth login` / `dial auth\nverify-otp` write the host auth file that the credential step below reads. Pinned\nto the same version the agent image gets, so host and sandbox agree:\n\n```nc:run effect:external\ncommand -v dial >/dev/null || npm install -g @getdial/cli@0.37.0\n```\n\n## Sign in to Dial\n\nDial's CLI owns the account credential (an auth file it writes on sign-in).\n\n### Check the host sign-in\n\nIs this host already signed in?\n\n```nc:run capture:signed_in=.auth.signedIn validate:^(true|false)$ effect:fetch\nDIAL_USER_AGENT={{dial_ua}} dial doctor --json\n```\n\n### Read the account\n\nIf it **is**, read which account — that account's key is what the chosen agents\nwill use:\n\n```nc:run capture:connected_email=.auth.email when:signed_in=true effect:fetch\nDIAL_USER_AGENT={{dial_ua}} dial doctor --json\n```\n```nc:operator when:signed_in=true\nThis host is signed in to Dial as {{connected_email}}; the agents you chose will use that account. To give them a different account, run `dial auth login <email> --force` and `dial auth verify-otp --code <code>` on the host first, then run /add-dial-tool again.\n```\n\n### Send the code\n\nIf it is **not**, verify an email with a one-time code. Collect the email:\n\n```nc:prompt owner_email validate:^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$ when:signed_in=false\nWhat's your email? Dial sends a one-time code to verify it. By continuing you create a Dial account and agree to Dial's Terms of Service (https://getdial.ai/terms) and Privacy Policy (https://getdial.ai/privacy).\n```\n\nSend the code (`--force` re-sends even if a prior code is pending):\n\n```nc:run effect:external when:signed_in=false\nDIAL_USER_AGENT={{dial_ua}} dial auth login {{owner_email}} --force\n```\n\n### Verify the code\n\nCollect the code:\n\n```nc:prompt otp validate:^\\d{6}$ when:signed_in=false\nEnter the 6-digit code from your email\n```\n\nVerify it. Do **not** pass `--agent nanoclaw` here: this skill owns the container\n`dial-cli` skill, and `--agent` would drop a second, unmanaged copy next to it:\n\n```nc:run effect:external when:signed_in=false\nDIAL_USER_AGENT={{dial_ua}} dial auth verify-otp --code {{otp}}\n```\n\n## Put the CLI and its skill in the agent image\n\nThe agent's global Node CLIs install from `container/cli-tools.json`, not from\nhand-edited Dockerfile layers. Add the pinned Dial CLI — idempotent on `name`, so\na re-run is a no-op. `@getdial/cli` has no native postinstall, so no `onlyBuilt`:\n\n```nc:json-merge into:container/cli-tools.json key:name\n{ \"name\": \"@getdial/cli\", \"version\": \"0.37.0\" }\n```\n\nThe version (`0.37.0`) is the canonical pin — this document is the source of\ntruth; the host install above uses the same one.\n\nMount the sandbox-aware `dial-cli` skill so the agent knows the CLI runs keyless\nin there and never asks for credentials. `container/skills/` is mounted read-only\ninto every agent container (at `/app/skills`) — which is why the key, not the\nskill file, is what gets scoped per agent:\n\n```nc:copy\ncontainer-skills/dial-cli/SKILL.md -> container/skills/dial-cli/SKILL.md\n```\n\nRebuild the image so the CLI lands. On an install that fetches a published image\nthis adds Dial as a layer on top of it; on one that builds its own it rebuilds:\n\n```nc:run effect:build\n./container/build.sh\n```\n\n## Register the credential with OneCLI\n\nRead the API key from the host auth file — the single source of truth, written\nby `dial auth login` / `dial auth verify-otp` — and put it in the OneCLI vault\nfor `api.getdial.ai`. Always **upsert**: the vault is keyed by name, so an\nexisting \"Dial API\" secret is not necessarily this account's (re-onboarding,\nswitching accounts, or rotating the key all leave a secret whose value points at\nthe previous account, and a sandboxed agent then lists *that* account's numbers).\nThe key travels through a `0600` temp file that is removed right after (`--file`), so it\nnever sits on a command line or in a captured variable:\n\n```nc:run effect:external\nT=$(mktemp) && chmod 600 \"$T\" && jq -r '.apiKey // empty' \"${XDG_DATA_HOME:-$HOME/.local/share}/dial/auth.v1.json\" > \"$T\" 2>/dev/null; [ -s \"$T\" ] || { rm -f \"$T\"; echo \"no Dial API key in the host auth file — sign in with dial auth login / verify-otp, then re-run\" >&2; exit 1; }; S=$(onecli secrets list | jq -r 'first(.data[] | select(.name | test(\"(?i)dial\"))) | .id // empty'); if [ -n \"$S\" ]; then onecli secrets update --id \"$S\" --file \"$T\" --host-pattern api.getdial.ai >/dev/null; else onecli secrets create --name \"Dial API\" --type generic --file \"$T\" --host-pattern api.getdial.ai --header-name Authorization --value-format \"Bearer {value}\" >/dev/null; fi; rc=$?; rm -f \"$T\"; exit $rc\n```\n\n## Scope it to the chosen agents\n\n### Create the OneCLI agents\n\nNanoClaw gives every agent group its own OneCLI agent whose `identifier` is the\ngroup id, created on the group's first spawn. A group that has never spawned has\nno OneCLI agent yet, and a block rule needs one to attach to — so create the\nmissing ones now, exactly as the runtime would (secret mode `all`, nothing else\ntouched):\n\n```nc:run effect:wire\nG=$(ncl groups list --json) || { echo \"could not list agent groups — is the NanoClaw host running?\" >&2; exit 1; }; AG=$(onecli agents list) || { echo \"could not list OneCLI agents\" >&2; exit 1; }; printf '%s' \"$G\" | jq -r '.data[] | \"\\(.id)\\t\\(.name)\"' | while IFS=\"$(printf '\\t')\" read -r gid gname; do printf '%s' \"$AG\" | jq -e --arg g \"$gid\" '.data[] | select(.identifier==$g)' >/dev/null || onecli agents create --name \"$gname\" --identifier \"$gid\" >/dev/null || { echo \"could not create an OneCLI agent for $gname ($gid)\" >&2; exit 1; }; done\n```\n\n### Set the block rules\n\nThe one switch is a per-agent **block rule** on `api.getdial.ai`, named\n`Dial: blocked for <group>` so only this skill's rules are ever read or written\n(an operator's own rules on the host are left alone). A chosen agent has its\nrule removed; every other agent has one present and enabled. A `403\nblocked_by_policy` in a container means \"not chosen\", not \"broken\":\n\n```nc:run effect:wire\nA=$(printf '%s' '{{dial_agents}}' | tr -d ' '); G=$(ncl groups list --json) || { echo \"could not list agent groups — is the NanoClaw host running?\" >&2; exit 1; }; case \",$A,\" in *,all,*) A=$(printf '%s' \"$G\" | jq -r '[.data[].id] | join(\",\")');; esac; AG=$(onecli agents list) || { echo \"could not list OneCLI agents\" >&2; exit 1; }; RL=$(onecli rules list) || { echo \"could not list OneCLI rules\" >&2; exit 1; }; printf '%s' \"$G\" | jq -r '.data[] | \"\\(.id)\\t\\(.name)\"' | while IFS=\"$(printf '\\t')\" read -r gid gname; do aid=$(printf '%s' \"$AG\" | jq -r --arg g \"$gid\" 'first(.data[] | select(.identifier==$g)) | .id // empty'); [ -n \"$aid\" ] || { echo \"no OneCLI agent for $gname ($gid)\" >&2; exit 1; }; rid=$(printf '%s' \"$RL\" | jq -r --arg a \"$aid\" 'first(.data[] | select(.hostPattern==\"api.getdial.ai\" and .action==\"block\" and .agentId==$a and (.name | startswith(\"Dial: blocked for \")) and ((.pathPattern // \"\")==\"\") and ((.method // \"\")==\"\"))) | .id // empty'); case \",$A,\" in *,\"$gid\",*) if [ -n \"$rid\" ]; then onecli rules delete --id \"$rid\" >/dev/null || { echo \"could not remove the Dial block for $gname ($gid)\" >&2; exit 1; }; fi; echo \"allowed: $gname ($gid)\";; *) if [ -z \"$rid\" ]; then onecli rules create --name \"Dial: blocked for $gname\" --host-pattern api.getdial.ai --action block --agent-id \"$aid\" --enabled >/dev/null || { echo \"could not create the Dial block for $gname ($gid)\" >&2; exit 1; }; else onecli rules update --id \"$rid\" --enabled true >/dev/null || { echo \"could not re-enable the Dial block for $gname ($gid)\" >&2; exit 1; }; fi; echo \"blocked: $gname ($gid)\";; esac; done\n```\n\n### Merge secrets for selective agents\n\nSecret lists are left alone, with one exception. An agent in `selective` mode only\ngets the secrets on its list, so a **chosen** selective agent has the Dial secret\nmerged into it. `onecli agents set-secrets` switches an agent to selective mode,\nso it is never called on an `all`-mode agent — that would silently cut the agent\noff from every credential not on its list. Blocked agents keep their lists\nuntouched in either mode; the rule alone blocks:\n\n```nc:run effect:wire\nA=$(printf '%s' '{{dial_agents}}' | tr -d ' '); case \",$A,\" in *,all,*) A=$(ncl groups list --json | jq -r '[.data[].id] | join(\",\")');; esac; S=$(onecli secrets list | jq -r 'first(.data[] | select(.name | test(\"(?i)dial\"))) | .id // empty'); [ -n \"$S\" ] || { echo \"no Dial secret in the OneCLI vault — the credential step above did not complete\" >&2; exit 1; }; onecli agents list | jq -r '.data[] | select(.secretMode==\"selective\") | \"\\(.id)\\t\\(.identifier)\"' | while IFS=\"$(printf '\\t')\" read -r aid gid; do case \",$A,\" in *,\"$gid\",*) onecli agents set-secrets --id \"$aid\" --secret-ids \"$(onecli agents secrets --id \"$aid\" | jq -r --arg s \"$S\" '[.data[], $s] | unique | join(\",\")')\" >/dev/null || { echo \"could not add the Dial secret to $gid\" >&2; exit 1; }; echo \"Dial secret added to the list of $gid\";; esac; done\n```\n\n## Hand the tool to running agents\n\n`container/skills/` is mounted read-only into every agent container, and each\ngroup's `.claude-shared/skills/` holds symlinks into that mount that are synced\nwhen the container spawns — so nothing is copied per session. A running agent\nkeeps its old image until it respawns, so restart every group; without a\n`--message` each one comes back on its next message, on the new image, with the\nCLI on `PATH` and the skill in place:\n\n```nc:run effect:external\nncl groups list --json | jq -r '.data[].id' | while read -r gid; do ncl groups restart --id \"$gid\" >/dev/null || { echo \"could not restart $gid\" >&2; exit 1; }; done\n```\n\n## Done\n\nThe chosen agents can now use Dial from inside their containers; the others are\nblocked at the gateway. Auth is injected by OneCLI; a `403 blocked_by_policy`\nmeans the agent was not chosen (run `/add-dial-tool` again to change that); a\n`401` means the Dial secret needs (re)connecting — not a login. Verify from a\nchat with a chosen agent: \"run dial doctor\" or \"text +1… hi\".\n\nTo uninstall: see [REMOVE.md](REMOVE.md). To wire Dial as a **messaging\nchannel** too, run `/add-dial`.\n\n## Troubleshooting\n\n**`command -v onecli` fails.** OneCLI is not installed or not on `PATH`. Run\n`/init-onecli`, then re-run this skill.\n\n**`ncl` can't reach the host.** The agent list and the scoping steps talk to the\nrunning NanoClaw service. Start it (`pnpm run dev`, or restart the service) and\nre-run.\n\n**`unknown agent group`.** An id in your answer is not in `ncl groups list`. Copy\nthe `ag-…` id exactly; names are not accepted.\n\n**`no Dial API key in the host auth file`.** The sign-in did not complete. Run\n`dial auth login <email> --force`, then `dial auth verify-otp --code <code>`, and\nre-run.\n\n**A chosen agent gets `401`.** The vault secret is stale (a different account's\nkey, or a rotated one). Re-run this skill — it always rewrites the secret with the\nkey the host is signed in with.\n\n**An agent you left out can still use Dial.** It was created after the last run\n(a new OneCLI agent starts in `all` mode with no rule). Re-run this skill; it\nonly touches the per-agent rules.\n\n**`dial: command not found` inside a container.** The image predates the manifest\nentry. Run `./container/build.sh`, then `ncl groups restart --id <group-id>` so the\nagent respawns on it.","author":"@nanocoai","ownerProfile":null,"authorContacts":null,"sourceUrl":"https://github.com/nanocoai/nanoclaw/tree/main/.claude/skills/add-dial-tool","license":"MIT","category":"writing","lang":"en","tokens":4207,"stars":0,"calls30d":2,"claimed":false,"visibility":"public","origin":"crawler","version":"0.1.0","createdAt":"2026-08-22","updatedAt":"2026-08-22","files":[{"path":"apply-fixtures.json","size":1675,"sha256":"777a132136e0f3e9811f7fe8aba39dd37414ddcd6ca4f6ba5389566cfdfbb7ca"},{"path":"REMOVE.md","size":2015,"sha256":"f54baba811dd86655dd31674cf7f7071b1757ed6891e0847eae627211ab35186"}],"requires":{"mcp":[],"tools":[]},"safety":{"flags":[],"scannedAt":"2026-08-22","hasScripts":false,"networkEndpoints":["getdial.ai"]}}