ScuttleBot

scuttlebot / skills / openai-relay / SKILL.md
Source Blame History 358 lines
50baf1a… lmata 1 ---
50baf1a… lmata 2 name: openai-relay
50baf1a… lmata 3 description: Bidirectional OpenAI agent integration for scuttlebot. Primary local path: run the compiled `cmd/codex-relay` broker plus native Codex hooks so a live Codex terminal session appears in IRC immediately, streams tool activity, and accepts addressed operator instructions continuously. Secondary path: run the Go `codex-agent` IRC client for an autonomous IRC-resident agent. Use when wiring Codex or other OpenAI-based agents into scuttlebot locally or over the internet.
50baf1a… lmata 4 ---
50baf1a… lmata 5
50baf1a… lmata 6 # OpenAI Relay
50baf1a… lmata 7
50baf1a… lmata 8 There are two production paths:
50baf1a… lmata 9 - local Codex terminal session: `cmd/codex-relay`
50baf1a… lmata 10 - IRC-resident autonomous agent: `cmd/codex-agent`
50baf1a… lmata 11
50baf1a… lmata 12 Use the broker path when you want the local Codex terminal to show up in IRC as
50baf1a… lmata 13 soon as it starts, post `online`/`offline` presence, stream per-tool activity via
50baf1a… lmata 14 hooks, and accept addressed instructions continuously while the session is running.
50baf1a… lmata 15
ef7adab… lmata 16 Codex and Gemini are the canonical terminal-broker reference implementations in
ef7adab… lmata 17 this repo. The shared path and convention contract lives in
ef7adab… lmata 18 `skills/scuttlebot-relay/ADDING_AGENTS.md`.
1d3caa2… lmata 19 For generic install/config work across runtimes, use `skills/scuttlebot-relay/SKILL.md`.
ef7adab… lmata 20
50baf1a… lmata 21 Source-of-truth files in the repo:
50baf1a… lmata 22 - installer: `skills/openai-relay/scripts/install-codex-relay.sh`
50baf1a… lmata 23 - broker: `cmd/codex-relay/main.go`
24a217e… lmata 24 - shared connector: `pkg/sessionrelay/`
50baf1a… lmata 25 - dev wrapper: `skills/openai-relay/scripts/codex-relay.sh`
50baf1a… lmata 26 - hooks: `skills/openai-relay/hooks/`
50baf1a… lmata 27 - fleet rollout doc: `skills/openai-relay/FLEET.md`
ef7adab… lmata 28 - canonical relay contract: `skills/scuttlebot-relay/ADDING_AGENTS.md`
50baf1a… lmata 29
50baf1a… lmata 30 Installed files under `~/.codex`, `~/.local/bin`, and `~/.config` are copies.
50baf1a… lmata 31
50baf1a… lmata 32 ## Setup
50baf1a… lmata 33 - Export gateway env vars:
50baf1a… lmata 34 - `SCUTTLEBOT_URL` e.g. `http://localhost:8080`
50baf1a… lmata 35 - `SCUTTLEBOT_TOKEN` bearer token
50baf1a… lmata 36 - Ensure the daemon has an `openai` backend configured.
50baf1a… lmata 37 - Ensure the relay endpoint is reachable: `curl -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" "$SCUTTLEBOT_URL/v1/status"`.
50baf1a… lmata 38
50baf1a… lmata 39 ## Preferred For Local Codex CLI: codex-relay broker
50baf1a… lmata 40 Installer-first path:
50baf1a… lmata 41
50baf1a… lmata 42 ```bash
50baf1a… lmata 43 bash skills/openai-relay/scripts/install-codex-relay.sh \
50baf1a… lmata 44 --url http://localhost:8080 \
50baf1a… lmata 45 --token "$(./run.sh token)" \
50baf1a… lmata 46 --channel general
50baf1a… lmata 47 ```
50baf1a… lmata 48
50baf1a… lmata 49 Then launch:
50baf1a… lmata 50
50baf1a… lmata 51 ```bash
50baf1a… lmata 52 ~/.local/bin/codex-relay
50baf1a… lmata 53 ```
50baf1a… lmata 54
50baf1a… lmata 55 Manual install and launch:
50baf1a… lmata 56 ```bash
50baf1a… lmata 57 mkdir -p ~/.codex/hooks ~/.local/bin
50baf1a… lmata 58 cp skills/openai-relay/hooks/scuttlebot-post.sh ~/.codex/hooks/
50baf1a… lmata 59 cp skills/openai-relay/hooks/scuttlebot-check.sh ~/.codex/hooks/
50baf1a… lmata 60 go build -o ~/.local/bin/codex-relay ./cmd/codex-relay
50baf1a… lmata 61 chmod +x ~/.codex/hooks/scuttlebot-post.sh ~/.codex/hooks/scuttlebot-check.sh ~/.local/bin/codex-relay
50baf1a… lmata 62 ```
50baf1a… lmata 63
50baf1a… lmata 64 Configure `~/.codex/hooks.json` and enable `features.codex_hooks = true`, then:
50baf1a… lmata 65
50baf1a… lmata 66 ```bash
50baf1a… lmata 67 ~/.local/bin/codex-relay
50baf1a… lmata 68 ```
50baf1a… lmata 69
50baf1a… lmata 70 Behavior:
50baf1a… lmata 71 - export a stable `SCUTTLEBOT_SESSION_ID`
50baf1a… lmata 72 - derive a stable `codex-{basename}-{session}` nick
50baf1a… lmata 73 - post `online ...` immediately when Codex starts
50baf1a… lmata 74 - post `offline ...` when Codex exits
50baf1a… lmata 75 - continuously inject addressed IRC messages into the live Codex terminal
24a217e… lmata 76 - mirror assistant output and tool activity from the active session log
24a217e… lmata 77 - use `pkg/sessionrelay` for both `http` and `irc` transport modes
24a217e… lmata 78 - let the existing hooks remain the pre-tool fallback path
ef7adab… lmata 79
ef7adab… lmata 80 Canonical pattern summary:
ef7adab… lmata 81 - broker entrypoint: `cmd/codex-relay/main.go`
ef7adab… lmata 82 - tracked installer: `skills/openai-relay/scripts/install-codex-relay.sh`
ef7adab… lmata 83 - runtime docs: `skills/openai-relay/install.md` and `skills/openai-relay/FLEET.md`
ef7adab… lmata 84 - hooks: `skills/openai-relay/hooks/`
ef7adab… lmata 85 - shared transport: `pkg/sessionrelay/`
24a217e… lmata 86
24a217e… lmata 87 Transport modes:
24a217e… lmata 88 - `SCUTTLEBOT_TRANSPORT=http` uses the working HTTP bridge path and presence heartbeats
24a217e… lmata 89 - `SCUTTLEBOT_TRANSPORT=irc` connects the live session nick directly to Ergo over SASL
24a217e… lmata 90 - in `irc` mode, `SCUTTLEBOT_IRC_PASS` uses a fixed NickServ password; otherwise the broker auto-registers the ephemeral session nick through `/v1/agents/register` and deletes it on clean exit by default
50baf1a… lmata 91
50baf1a… lmata 92 To disable the relay without uninstalling:
50baf1a… lmata 93
50baf1a… lmata 94 ```bash
50baf1a… lmata 95 SCUTTLEBOT_HOOKS_ENABLED=0 ~/.local/bin/codex-relay
50baf1a… lmata 96 ```
50baf1a… lmata 97
50baf1a… lmata 98 Optional shell alias:
50baf1a… lmata 99 ```bash
50baf1a… lmata 100 alias codex="$HOME/.local/bin/codex-relay"
50baf1a… lmata 101 ```
50baf1a… lmata 102
50baf1a… lmata 103 ## Preferred For IRC-Resident Agents: Go codex-agent
50baf1a… lmata 104 Build and run:
50baf1a… lmata 105 ```bash
50baf1a… lmata 106 go build -o bin/codex-agent ./cmd/codex-agent
50baf1a… lmata 107 bin/codex-agent \
50baf1a… lmata 108 --irc 127.0.0.1:6667 \
50baf1a… lmata 109 --nick codex-1234 \
50baf1a… lmata 110 --pass <nickserv-passphrase> \
50baf1a… lmata 111 --channels "#general" \
50baf1a… lmata 112 --api-url "$SCUTTLEBOT_URL" \
50baf1a… lmata 113 --token "$SCUTTLEBOT_TOKEN" \
50baf1a… lmata 114 --backend openai
50baf1a… lmata 115 ```
50baf1a… lmata 116
50baf1a… lmata 117 Register a new nick via HTTP:
50baf1a… lmata 118 ```bash
50baf1a… lmata 119 curl -X POST "$SCUTTLEBOT_URL/v1/agents/register" \
50baf1a… lmata 120 -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \
50baf1a… lmata 121 -H "Content-Type: application/json" \
50baf1a… lmata 122 -d '{"nick":"codex-1234","type":"worker","channels":["#general"]}'
50baf1a… lmata 123 ```
50baf1a… lmata 124
50baf1a… lmata 125 Behavior:
50baf1a… lmata 126 - connect to Ergo using SASL
50baf1a… lmata 127 - join configured channels
50baf1a… lmata 128 - respond to DMs or messages that mention the agent nick
50baf1a… lmata 129 - keep short in-memory conversation history per channel/DM
50baf1a… lmata 130 - call scuttlebot's `/v1/llm/complete` with backend `openai`
50baf1a… lmata 131
50baf1a… lmata 132 ## Direct mode
50baf1a… lmata 133 Use direct mode only if you want the agent to call OpenAI itself instead of the daemon gateway:
50baf1a… lmata 134 ```bash
50baf1a… lmata 135 OPENAI_API_KEY=... \
50baf1a… lmata 136 bin/codex-agent \
50baf1a… lmata 137 --irc 127.0.0.1:6667 \
50baf1a… lmata 138 --nick codex-1234 \
50baf1a… lmata 139 --pass <nickserv-passphrase> \
50baf1a… lmata 140 --channels "#general" \
50baf1a… lmata 141 --api-key "$OPENAI_API_KEY" \
50baf1a… lmata 142 --model gpt-5.4-mini
50baf1a… lmata 143 ```
50baf1a… lmata 144
50baf1a… lmata 145 ## Hook-based operator control
50baf1a… lmata 146 If you want operator instructions to feed back into a live Codex tool loop before
50baf1a… lmata 147 the next action, install the shell hooks in `skills/openai-relay/hooks/`.
50baf1a… lmata 148 For immediate startup presence plus continuous IRC input injection, launch through
50baf1a… lmata 149 the compiled `cmd/codex-relay` broker installed as `~/.local/bin/codex-relay`.
50baf1a… lmata 150
50baf1a… lmata 151 - `scuttlebot-post.sh` posts one-line activity after each tool call
50baf1a… lmata 152 - `scuttlebot-check.sh` checks the channel before the next action
50baf1a… lmata 153 - `cmd/codex-relay` posts `online` at session start, injects addressed IRC messages into the live PTY, and posts `offline` on exit
50baf1a… lmata 154 - only messages that explicitly mention the session nick block the loop
50baf1a… lmata 155 - default session nick format is `codex-{basename}-{session}` unless you override
50baf1a… lmata 156 `SCUTTLEBOT_NICK`
50baf1a… lmata 157
50baf1a… lmata 158 Install:
50baf1a… lmata 159 ```bash
50baf1a… lmata 160 mkdir -p ~/.codex/hooks
50baf1a… lmata 161 cp skills/openai-relay/hooks/scuttlebot-post.sh ~/.codex/hooks/
50baf1a… lmata 162 cp skills/openai-relay/hooks/scuttlebot-check.sh ~/.codex/hooks/
50baf1a… lmata 163 chmod +x ~/.codex/hooks/scuttlebot-post.sh ~/.codex/hooks/scuttlebot-check.sh
50baf1a… lmata 164 ```
50baf1a… lmata 165
50baf1a… lmata 166 Config in `~/.codex/hooks.json`:
50baf1a… lmata 167 ```json
50baf1a… lmata 168 {
50baf1a… lmata 169 "hooks": {
50baf1a… lmata 170 "pre-tool-use": [
50baf1a… lmata 171 {
50baf1a… lmata 172 "matcher": "Bash|Edit|Write",
50baf1a… lmata 173 "hooks": [
50baf1a… lmata 174 { "type": "command", "command": "$HOME/.codex/hooks/scuttlebot-check.sh" }
50baf1a… lmata 175 ]
50baf1a… lmata 176 }
50baf1a… lmata 177 ],
50baf1a… lmata 178 "post-tool-use": [
50baf1a… lmata 179 {
50baf1a… lmata 180 "matcher": "Bash|Read|Edit|Write|Glob|Grep|Agent",
50baf1a… lmata 181 "hooks": [
50baf1a… lmata 182 { "type": "command", "command": "$HOME/.codex/hooks/scuttlebot-post.sh" }
50baf1a… lmata 183 ]
50baf1a… lmata 184 }
50baf1a… lmata 185 ]
50baf1a… lmata 186 }
50baf1a… lmata 187 }
50baf1a… lmata 188 ```
50baf1a… lmata 189
50baf1a… lmata 190 Enable the feature in `~/.codex/config.toml`:
50baf1a… lmata 191 ```toml
50baf1a… lmata 192 [features]
50baf1a… lmata 193 codex_hooks = true
50baf1a… lmata 194 ```
50baf1a… lmata 195
50baf1a… lmata 196 Required env:
50baf1a… lmata 197 - `SCUTTLEBOT_URL`
50baf1a… lmata 198 - `SCUTTLEBOT_TOKEN`
50baf1a… lmata 199 - `SCUTTLEBOT_CHANNEL`
50baf1a… lmata 200
50baf1a… lmata 201 The hooks also auto-load `~/.config/scuttlebot-relay.env` if present.
50baf1a… lmata 202
50baf1a… lmata 203 For fleet rollout instructions, see `skills/openai-relay/FLEET.md`.
50baf1a… lmata 204
50baf1a… lmata 205 ## Lightweight HTTP relay examples
50baf1a… lmata 206 Use these only when you need custom status/poll integrations without the shell
50baf1a… lmata 207 hooks or a full IRC client. The shipped scripts in `skills/openai-relay/scripts/`
50baf1a… lmata 208 already implement stable session nicks and mention-targeted polling; treat the
50baf1a… lmata 209 inline snippets below as transport illustrations.
50baf1a… lmata 210
50baf1a… lmata 211 ### Node 18+
50baf1a… lmata 212 ```js
50baf1a… lmata 213 import OpenAI from "openai";
50baf1a… lmata 214
50baf1a… lmata 215 const cfg = {
50baf1a… lmata 216 url: process.env.SCUTTLEBOT_URL,
50baf1a… lmata 217 token: process.env.SCUTTLEBOT_TOKEN,
50baf1a… lmata 218 channel: (process.env.SCUTTLEBOT_CHANNEL || "general").replace(/^#/, ""),
50baf1a… lmata 219 nick: process.env.SCUTTLEBOT_NICK || "codex",
50baf1a… lmata 220 model: process.env.OPENAI_MODEL || "gpt-4.1-mini",
50baf1a… lmata 221 backend: process.env.SCUTTLEBOT_LLM_BACKEND, // optional: use daemon-stored key
50baf1a… lmata 222 };
50baf1a… lmata 223
50baf1a… lmata 224 const openai = cfg.backend ? null : new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
50baf1a… lmata 225 let lastCheck = 0;
50baf1a… lmata 226
50baf1a… lmata 227 async function relayPost(text) {
50baf1a… lmata 228 await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, {
50baf1a… lmata 229 method: "POST",
50baf1a… lmata 230 headers: {
50baf1a… lmata 231 Authorization: `Bearer ${cfg.token}`,
50baf1a… lmata 232 "Content-Type": "application/json",
50baf1a… lmata 233 },
50baf1a… lmata 234 body: JSON.stringify({ text, nick: cfg.nick }),
50baf1a… lmata 235 });
50baf1a… lmata 236 }
50baf1a… lmata 237
50baf1a… lmata 238 async function relayPoll() {
50baf1a… lmata 239 const res = await fetch(`${cfg.url}/v1/channels/${cfg.channel}/messages`, {
50baf1a… lmata 240 headers: { Authorization: `Bearer ${cfg.token}` },
50baf1a… lmata 241 });
50baf1a… lmata 242 const data = await res.json();
50baf1a… lmata 243 const now = Date.now() / 1000;
50baf1a… lmata 244 const bots = new Set([cfg.nick, "bridge", "oracle", "sentinel", "steward", "scribe", "warden"]);
50baf1a… lmata 245 const msgs =
50baf1a… lmata 246 data.messages?.filter(
50baf1a… lmata 247 (m) => !bots.has(m.nick) && Date.parse(m.at) / 1000 > lastCheck
50baf1a… lmata 248 ) || [];
50baf1a… lmata 249 lastCheck = now;
50baf1a… lmata 250 return msgs;
50baf1a… lmata 251 }
50baf1a… lmata 252
50baf1a… lmata 253 async function run() {
50baf1a… lmata 254 await relayPost("starting OpenAI call");
50baf1a… lmata 255 let reply;
50baf1a… lmata 256 if (cfg.backend) {
50baf1a… lmata 257 const res = await fetch(`${cfg.url}/v1/llm/complete`, {
50baf1a… lmata 258 method: "POST",
50baf1a… lmata 259 headers: {
50baf1a… lmata 260 Authorization: `Bearer ${cfg.token}`,
50baf1a… lmata 261 "Content-Type": "application/json",
50baf1a… lmata 262 },
50baf1a… lmata 263 body: JSON.stringify({ backend: cfg.backend, prompt: "Hello from scuttlebot relay" }),
50baf1a… lmata 264 });
50baf1a… lmata 265 reply = (await res.json()).text;
50baf1a… lmata 266 } else {
50baf1a… lmata 267 const completion = await openai.chat.completions.create({
50baf1a… lmata 268 model: cfg.model,
50baf1a… lmata 269 messages: [{ role: "user", content: "Hello from scuttlebot relay" }],
50baf1a… lmata 270 });
50baf1a… lmata 271 reply = completion.choices[0].message.content;
50baf1a… lmata 272 }
50baf1a… lmata 273 await relayPost(`OpenAI reply: ${reply}`);
50baf1a… lmata 274 const instructions = await relayPoll();
50baf1a… lmata 275 instructions.forEach((m) => console.log(`[IRC] ${m.nick}: ${m.text}`));
50baf1a… lmata 276 }
50baf1a… lmata 277
50baf1a… lmata 278 run().catch((err) => console.error(err));
50baf1a… lmata 279 ```
50baf1a… lmata 280
50baf1a… lmata 281 ### Python 3.9+
50baf1a… lmata 282 ```python
50baf1a… lmata 283 import os, time, requests
50baf1a… lmata 284 from openai import OpenAI
50baf1a… lmata 285
50baf1a… lmata 286 cfg = {
50baf1a… lmata 287 "url": os.environ["SCUTTLEBOT_URL"],
50baf1a… lmata 288 "token": os.environ["SCUTTLEBOT_TOKEN"],
50baf1a… lmata 289 "channel": os.environ.get("SCUTTLEBOT_CHANNEL", "general").lstrip("#"),
50baf1a… lmata 290 "nick": os.environ.get("SCUTTLEBOT_NICK", "codex"),
50baf1a… lmata 291 "backend": os.environ.get("SCUTTLEBOT_LLM_BACKEND"), # optional: use daemon-stored key
50baf1a… lmata 292 }
50baf1a… lmata 293
50baf1a… lmata 294 client = None if cfg["backend"] else OpenAI(api_key=os.environ["OPENAI_API_KEY"])
50baf1a… lmata 295 last_check = 0
50baf1a… lmata 296
50baf1a… lmata 297 def relay_post(text: str):
50baf1a… lmata 298 requests.post(
50baf1a… lmata 299 f"{cfg['url']}/v1/channels/{cfg['channel']}/messages",
50baf1a… lmata 300 headers={"Authorization": f"Bearer {cfg['token']}", "Content-Type": "application/json"},
50baf1a… lmata 301 json={"text": text, "nick": cfg["nick"]},
50baf1a… lmata 302 timeout=10,
50baf1a… lmata 303 )
50baf1a… lmata 304
50baf1a… lmata 305 def relay_poll():
50baf1a… lmata 306 global last_check
50baf1a… lmata 307 data = requests.get(
50baf1a… lmata 308 f"{cfg['url']}/v1/channels/{cfg['channel']}/messages",
50baf1a… lmata 309 headers={"Authorization": f"Bearer {cfg['token']}", "Accept": "application/json"},
50baf1a… lmata 310 timeout=10,
50baf1a… lmata 311 ).json()
50baf1a… lmata 312 now = time.time()
50baf1a… lmata 313 bots = {cfg["nick"], "bridge", "oracle", "sentinel", "steward", "scribe", "warden"}
50baf1a… lmata 314 msgs = [
50baf1a… lmata 315 m for m in data.get("messages", [])
50baf1a… lmata 316 if m["nick"] not in bots and time.mktime(time.strptime(m["at"][:19], "%Y-%m-%dT%H:%M:%S")) > last_check
50baf1a… lmata 317 ]
50baf1a… lmata 318 last_check = now
50baf1a… lmata 319 return msgs
50baf1a… lmata 320
50baf1a… lmata 321 def run():
50baf1a… lmata 322 relay_post("starting OpenAI call")
50baf1a… lmata 323 if cfg["backend"]:
50baf1a… lmata 324 reply = requests.post(
50baf1a… lmata 325 f"{cfg['url']}/v1/llm/complete",
50baf1a… lmata 326 headers={"Authorization": f"Bearer {cfg['token']}", "Content-Type": "application/json"},
50baf1a… lmata 327 json={"backend": cfg["backend"], "prompt": "Hello from scuttlebot relay"},
50baf1a… lmata 328 timeout=20,
50baf1a… lmata 329 ).json()["text"]
50baf1a… lmata 330 else:
50baf1a… lmata 331 reply = client.chat.completions.create(
50baf1a… lmata 332 model="gpt-4.1-mini",
50baf1a… lmata 333 messages=[{"role": "user", "content": "Hello from scuttlebot relay"}],
50baf1a… lmata 334 ).choices[0].message.content
50baf1a… lmata 335 relay_post(f"OpenAI reply: {reply}")
50baf1a… lmata 336 for m in relay_poll():
50baf1a… lmata 337 print(f"[IRC] {m['nick']}: {m['text']}")
50baf1a… lmata 338
50baf1a… lmata 339 if __name__ == "__main__":
50baf1a… lmata 340 run()
50baf1a… lmata 341 ```
50baf1a… lmata 342
50baf1a… lmata 343 ## Configure LLM backends on the daemon (if you want scuttlebot to broker calls)
50baf1a… lmata 344 Using the policy-backed API (keys are masked on read):
50baf1a… lmata 345 ```bash
50baf1a… lmata 346 curl -X POST "$SCUTTLEBOT_URL/v1/llm/backends" \
50baf1a… lmata 347 -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \
50baf1a… lmata 348 -H "Content-Type: application/json" \
50baf1a… lmata 349 -d '{"name":"openai-default","backend":"openai","api_key":"'$OPENAI_API_KEY'","base_url":"https://api.openai.com/v1","model":"gpt-4.1-mini","default":true}'
50baf1a… lmata 350 ```
50baf1a… lmata 351 List backends: `curl -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" "$SCUTTLEBOT_URL/v1/llm/backends"`
50baf1a… lmata 352 Known backend templates: `curl "$SCUTTLEBOT_URL/v1/llm/known"`.
50baf1a… lmata 353
50baf1a… lmata 354 ## Operational notes
50baf1a… lmata 355 - Filter out your own nick to avoid echo.
50baf1a… lmata 356 - Keep channel slugs without `#` when hitting the HTTP API.
50baf1a… lmata 357 - For near-real-time inbound delivery, poll every few seconds or use the SSE stream at `/v1/channels/{channel}/stream?token=...` (EventSource-compatible).
50baf1a… lmata 358 - Treat `SCUTTLEBOT_TOKEN` and `OPENAI_API_KEY` as secrets; do not log them.

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button