diff --git a/.devcontainer/entrypoint.sh b/.devcontainer/entrypoint.sh index a88399c..6d60d12 100644 --- a/.devcontainer/entrypoint.sh +++ b/.devcontainer/entrypoint.sh @@ -208,6 +208,29 @@ if [ -f "$HARNESS/scripts/cron-runtime.ts" ] && command -v tmux &>/dev/null; the fi fi +# ─── Restore client-slack session if Slack is configured ────────────── +# Tokens live in .devcontainer/.env (Compose KEY=value format). When both +# SLACK_APP_TOKEN and SLACK_BOT_TOKEN are set AND `pi` is installed for +# the sandbox user, restore the session that `oh config slack` created. +# Sourcing happens inside the tmux command (same pattern the wizard uses) +# so the child `pi` process inherits the exported vars. +SLACK_ENV="$HARNESS/.devcontainer/.env" +if [ -f "$SLACK_ENV" ] && command -v tmux &>/dev/null; then + # shellcheck disable=SC1090 + SLACK_APP_TOKEN=$(grep -E '^SLACK_APP_TOKEN=' "$SLACK_ENV" | tail -1 | cut -d= -f2-) + SLACK_BOT_TOKEN=$(grep -E '^SLACK_BOT_TOKEN=' "$SLACK_ENV" | tail -1 | cut -d= -f2-) + if [ -n "$SLACK_APP_TOKEN" ] && [ -n "$SLACK_BOT_TOKEN" ] \ + && gosu sandbox bash -lc 'command -v pi' &>/dev/null; then + if ! gosu sandbox tmux has-session -t client-slack 2>/dev/null; then + gosu sandbox tmux new-session -d -s client-slack \ + "bash -c 'set -a; source $SLACK_ENV; set +a; pi 2>&1 | tee /tmp/client-slack.log'" + echo "[entrypoint] client-slack tmux session started (Slack bridge)" + else + echo "[entrypoint] client-slack tmux session already running — skipping" + fi + fi +fi + # ─── Optional: agent-browser (opt-in via INSTALL_AGENT_BROWSER=true) ── if [ "${INSTALL_AGENT_BROWSER:-false}" = "true" ] && ! command -v agent-browser &>/dev/null; then echo "[entrypoint] Installing agent-browser (INSTALL_AGENT_BROWSER=true)..." diff --git a/.pi/extensions/slack/README.md b/.pi/extensions/slack/README.md index b35efac..3f0f401 100644 --- a/.pi/extensions/slack/README.md +++ b/.pi/extensions/slack/README.md @@ -74,8 +74,8 @@ SLACK_ALLOW_USERS=U789 ```bash make shell set -a; source /home/sandbox/harness/.devcontainer/.env; set +a - tmux new-session -d -s agent-pi 'pi 2>&1 | tee /tmp/agent-pi.log' - tmux attach -t agent-pi + tmux new-session -d -s client-slack 'pi 2>&1 | tee /tmp/client-slack.log' + tmux attach -t client-slack ``` `set -a` is needed because `.devcontainer/.env` is Compose-formatted (no `export` prefix); without it, sourced vars stay in the shell but aren't inherited by `pi`. The tmux session keeps `pi` alive across SSH/VS Code disconnects (see [`context/rules/sandbox-processes.md`](../../context/rules/sandbox-processes.md)). @@ -86,7 +86,7 @@ SLACK_ALLOW_USERS=U789 ```bash env | grep SLACK # confirm vars are exported ``` - Then DM the bot or `@mention` it from an allow-listed user. Watch the round-trip in `tmux attach -t agent-pi` or `tail -f /tmp/agent-pi.log`. + Then DM the bot or `@mention` it from an allow-listed user. Watch the round-trip in `tmux attach -t client-slack` or `tail -f /tmp/client-slack.log`. ## Build and Test diff --git a/CHANGELOG.md b/CHANGELOG.md index 30feb97..83efd68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ Update policy and release automation live in [`.claude/rules/git.md`](.claude/ru - `README.md`, `docs/installation.md`, and `docs/quickstart.md` restructured to surface fork-and-clone and clone-and-own as first-class install paths alongside the upstream one-liner; `scripts/install.sh` local-run detection is now the documented bootstrap for self-hosters ([#331](https://github.com/ryaneggz/open-harness/issues/331)). - Skill `LICENSE` files updated to `Copyright (c) 2026 Mifune Dev (mifune.dev)` to match upstream correction. - Re-synced `context-audit` to the portable upstream form ([mifunedev/skills#6](https://github.com/mifunedev/skills/pull/6)): removed the top-level `argument-hint` frontmatter key, repinned `.mifune/skills.lock` (`commit` `f95011fe`, checksum `02dd6324`), and added the previously-missing `LICENSE` for folder parity ([#339](https://github.com/ryaneggz/open-harness/issues/339)). +- Renamed the Slack-bridge tmux session from `agent-pi` to `client-slack` and introduced a `client-` prefix in `context/rules/sandbox-processes.md` for external-surface clients (Slack, Discord, web UIs) that bridge an in-sandbox agent. `.devcontainer/entrypoint.sh` now restores the `client-slack` session on container start whenever `SLACK_APP_TOKEN` and `SLACK_BOT_TOKEN` are present in `.devcontainer/.env` and `pi` is installed, so the Slack bridge survives `docker compose restart` without manual relaunch. Rename propagated through `apps/oh/src/config/slack.ts` (`oh config slack` wizard), `.pi/extensions/slack/README.md`, `docs/integrations/slack.md`, `docs/connecting.md`, `docs/intro.md`, and `docs/harnesses/overview.md` (the latter folding in the half-finished `harness-pi` rename from #302). ### Fixed - Sandbox image installs `pi-coding-agent` under the sandbox user's npm prefix (`/home/sandbox/.local`) and places that bin directory before `/usr/bin`, allowing `pi update` to self-update without sudo on fresh builds. ### Removed diff --git a/apps/oh/src/config/slack.ts b/apps/oh/src/config/slack.ts index 5989878..b1a36ba 100644 --- a/apps/oh/src/config/slack.ts +++ b/apps/oh/src/config/slack.ts @@ -10,7 +10,7 @@ import { hasSession, killSession, newSession, capturePane, isInstalled as tmuxAvailable, } from "../lib/tmux.js"; -const AGENT_PI_SESSION = "agent-pi"; +const CLIENT_SLACK_SESSION = "client-slack"; const CONNECTED_MARKER = "connected and listening"; const POLL_INTERVAL_MS = 1000; const MAX_POLLS = 15; @@ -143,37 +143,37 @@ async function promptAllowlist(env: Record): Promise return result; } -async function relaunchAgentPi(envPath: string): Promise { +async function relaunchClientSlack(envPath: string): Promise { if (!tmuxAvailable()) { - warn("tmux not found in PATH — skipping agent-pi restart. Restart it manually after this exits."); + warn("tmux not found in PATH — skipping client-slack restart. Restart it manually after this exits."); return; } - const wasRunning = hasSession(AGENT_PI_SESSION); + const wasRunning = hasSession(CLIENT_SLACK_SESSION); if (wasRunning) { - info(`Killing existing tmux session "${AGENT_PI_SESSION}"…`); - killSession(AGENT_PI_SESSION); + info(`Killing existing tmux session "${CLIENT_SLACK_SESSION}"…`); + killSession(CLIENT_SLACK_SESSION); } - info(`Starting tmux session "${AGENT_PI_SESSION}"…`); + info(`Starting tmux session "${CLIENT_SLACK_SESSION}"…`); // The session sources .env via `set -a` so child pi inherits the new vars. - const cmd = `bash -c 'set -a; source ${envPath}; set +a; pi 2>&1 | tee /tmp/agent-pi.log'`; - newSession(AGENT_PI_SESSION, cmd); + const cmd = `bash -c 'set -a; source ${envPath}; set +a; pi 2>&1 | tee /tmp/client-slack.log'`; + newSession(CLIENT_SLACK_SESSION, cmd); info("Validating Slack connection (up to 15s)…"); for (let i = 0; i < MAX_POLLS; i++) { - const out = capturePane(AGENT_PI_SESSION); + const out = capturePane(CLIENT_SLACK_SESSION); if (out.includes(CONNECTED_MARKER)) { ok("Slack extension connected."); return; } if (/Run error|Missing env/.test(out)) { - fail("Slack extension reported an error. Check `tmux attach -t agent-pi`."); + fail("Slack extension reported an error. Check `tmux attach -t client-slack`."); return; } await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS)); process.stdout.write("."); } process.stdout.write("\n"); - warn("Did not see 'connected and listening' within 15s. Check `tmux attach -t agent-pi`."); + warn("Did not see 'connected and listening' within 15s. Check `tmux attach -t client-slack`."); } export async function runSlack(): Promise { @@ -231,20 +231,20 @@ export async function runSlack(): Promise { return 1; } - info("Next step: start the Slack bridge by (re)launching the agent-pi tmux session."); - info("This kills any existing agent-pi session, then starts pi with the new tokens;"); + info("Next step: start the Slack bridge by (re)launching the client-slack tmux session."); + info("This kills any existing client-slack session, then starts pi with the new tokens;"); info("pi loads the slack extension on boot and opens the Socket Mode connection."); if (await confirm("Start the Slack bridge now?")) { - await relaunchAgentPi(envPath); + await relaunchClientSlack(envPath); info(""); info("Slack bridge is live. Test it: DM your bot or @mention it in an allow-listed channel."); - info("Tail the live log: tmux attach -t agent-pi (detach: Ctrl-b d)"); + info("Tail the live log: tmux attach -t client-slack (detach: Ctrl-b d)"); } else { info("Skipped. Start the bridge manually when ready:"); - info(" tmux kill-session -t agent-pi 2>/dev/null; \\"); + info(" tmux kill-session -t client-slack 2>/dev/null; \\"); info(` set -a; source ${envPath}; set +a; \\`); - info(" tmux new-session -d -s agent-pi 'pi 2>&1 | tee /tmp/agent-pi.log'"); + info(" tmux new-session -d -s client-slack 'pi 2>&1 | tee /tmp/client-slack.log'"); } return 0; diff --git a/context/rules/sandbox-processes.md b/context/rules/sandbox-processes.md index 7927566..ae8c23e 100644 --- a/context/rules/sandbox-processes.md +++ b/context/rules/sandbox-processes.md @@ -18,7 +18,8 @@ Format: `-` (kebab-case inside each segment). |----------|---------|---------| | `app-` | `app-docs`, `app-api` | User dev servers | | `expose-public-` | `expose-public-3000` | Cloudflare quick tunnels (auto-created) | -| `agent-` | `agent-claude`, `agent-pi` | AI agent processes | +| `agent-` | `agent-watcher`, `agent-batch` | Headless / long-running agent processes. Interactive CLIs (`claude`, `codex`, `opencode`) are normally foreground in a terminal or VS Code, not detached in tmux. | +| `client-` | `client-slack`, `client-discord` | External-surface clients that bridge an in-sandbox agent to a third-party UI | | `heartbeat-` | `heartbeat-daily-sync` | Scheduled heartbeat tasks | Reserved prefix: `system-`. Do not use for user apps. diff --git a/docs/connecting.md b/docs/connecting.md index 4957fac..2f802d6 100644 --- a/docs/connecting.md +++ b/docs/connecting.md @@ -93,7 +93,8 @@ All long-running processes inside the sandbox run in named tmux sessions. The na | Category | Example | Purpose | |----------|---------|---------| -| `agent-` | `agent-claude`, `agent-pi` | AI agent processes | +| `client-` | `client-slack`, `client-discord` | External-surface clients bridging an in-sandbox agent | +| `agent-` | `agent-watcher`, `agent-batch` | Headless / long-running agent processes (interactive CLIs are foreground, not tmux) | | `app-` | `app-docs`, `app-api` | Dev servers | For the full convention see [`context/rules/sandbox-processes.md`](https://github.com/ryaneggz/open-harness/blob/development/context/rules/sandbox-processes.md). @@ -113,12 +114,12 @@ Follow Option B (or C for a remote host). The Ports panel in VSCode shows forwar oh config slack ``` -The interactive wizard collects your Slack tokens, validates them, writes `.devcontainer/.env`, and restarts the `agent-pi` tmux session in Socket Mode. For the full wizard walkthrough see [Integrations → Slack](/docs/integrations/slack). +The interactive wizard collects your Slack tokens, validates them, writes `.devcontainer/.env`, and restarts the `client-slack` tmux session in Socket Mode. For the full wizard walkthrough see [Integrations → Slack](/docs/integrations/slack). After the wizard completes, verify the bridge is live: ```bash -tmux capture-pane -t agent-pi -p | grep -i 'connected\|listening' +tmux capture-pane -t client-slack -p | grep -i 'connected\|listening' ``` ### Step 3 — Launch T3 Code diff --git a/docs/harnesses/overview.md b/docs/harnesses/overview.md index 1763348..7f50918 100644 --- a/docs/harnesses/overview.md +++ b/docs/harnesses/overview.md @@ -54,11 +54,11 @@ Each runs in its own named tmux session per [`context/rules/sandbox-processes.md ### Pi+Slack -The Pi agent with the Slack bridge loaded. The wizard writes `.devcontainer/.env`, launches the `harness-pi` session, and waits for the Socket Mode connection to come up: +The Pi agent with the Slack bridge loaded. The wizard writes `.devcontainer/.env`, launches the `client-slack` session, and waits for the Socket Mode connection to come up: ```bash oh config slack # interactive wizard -tmux attach -t harness-pi # watch the live log +tmux attach -t client-slack # watch the live log ``` Talk to the agent from Slack (DM or `@mention` in an allow-listed channel). Full setup: [Slack integration](../integrations/slack.md). diff --git a/docs/integrations/slack.md b/docs/integrations/slack.md index 5b9e789..fd4452e 100644 --- a/docs/integrations/slack.md +++ b/docs/integrations/slack.md @@ -69,7 +69,7 @@ Keep both values ready for the next step. Inside the sandbox, run the wizard. It prompts for the two tokens, asks which allowlist mode to use (users / channels / both), validates IDs, writes `.devcontainer/.env` atomically, and offers to restart the -`agent-pi` tmux session. +`client-slack` tmux session. ```bash make shell # enter the sandbox @@ -84,7 +84,7 @@ The wizard: - Writes only the Slack keys; unrelated entries (`GH_TOKEN`, `TZ`, etc.) are preserved. - **Starts the Slack bridge for you** — at the end, it kills any - existing `agent-pi` tmux session and launches a fresh one running + existing `client-slack` tmux session and launches a fresh one running `pi` with the new env sourced. `pi` loads the Slack extension on boot and opens the Socket Mode connection. The wizard polls for the `"connected and listening"` log line and prints `✓ Slack bridge is live` @@ -99,7 +99,7 @@ If `oh` isn't available (older sandbox image, or you'd rather edit by hand), the manual procedure still works:
-Hand-edit .devcontainer/.env + relaunch agent-pi +Hand-edit .devcontainer/.env + relaunch client-slack `.devcontainer/.env` uses Docker Compose `KEY=value` format — no `export` prefix. This file is gitignored, so your tokens will not be committed. @@ -126,11 +126,11 @@ vars. This is the most common manual-mode failure. ```bash set -a; source /home/sandbox/harness/.devcontainer/.env; set +a -tmux new-session -d -s agent-pi 'pi 2>&1 | tee /tmp/agent-pi.log' -tmux attach -t agent-pi +tmux new-session -d -s client-slack 'pi 2>&1 | tee /tmp/client-slack.log' +tmux attach -t client-slack ``` -Detach with `Ctrl-b d`. The session name `agent-pi` follows the `agent-` +Detach with `Ctrl-b d`. The session name `client-slack` follows the `client-` prefix convention in [`context/rules/sandbox-processes.md`](https://github.com/ryaneggz/open-harness/blob/development/context/rules/sandbox-processes.md). @@ -151,7 +151,7 @@ the env (before attaching to tmux). 2. **Socket Mode connected** (the real connectivity check): ```bash - tmux capture-pane -t agent-pi -p | grep -i 'socket\|connected\|listening' + tmux capture-pane -t client-slack -p | grep -i 'socket\|connected\|listening' ``` Look for lines indicating the Socket Mode client connected or is listening. Note: `curl https://slack.com/api/auth.test` only validates the bot token @@ -161,17 +161,17 @@ the env (before attaching to tmux). 3. **Round-trip test:** From an allow-listed user account, DM the bot or `@mention` it in an - allow-listed channel. Watch `tmux attach -t agent-pi` — you should see the + allow-listed channel. Watch `tmux attach -t client-slack` — you should see the inbound event logged and the agent's reply posted back to Slack. ## 7. Troubleshooting | Symptom | Cause | Fix | |---------|-------|-----| -| Bot doesn't respond; allowlist is set | `pi` did not inherit the vars — `set -a` ran in a different shell than `tmux new-session` | Kill the session (`tmux kill-session -t agent-pi`), run `set -a; source …; set +a`, then relaunch `tmux new-session` in the same shell | +| Bot doesn't respond; allowlist is set | `pi` did not inherit the vars — `set -a` ran in a different shell than `tmux new-session` | Kill the session (`tmux kill-session -t client-slack`), run `set -a; source …; set +a`, then relaunch `tmux new-session` in the same shell | | Bot doesn't respond; no allowlist set | Deny-default: both `SLACK_ALLOW_CHANNELS` and `SLACK_ALLOW_USERS` are unset | Add at least one allowlist var to `.devcontainer/.env` and relaunch | | `invalid_auth` or `not_authed` in log | `xapp-` and `xoxb-` tokens are swapped | `SLACK_APP_TOKEN` must be the `xapp-` token; `SLACK_BOT_TOKEN` must be the `xoxb-` token — correct in `.devcontainer/.env` and relaunch | -| Socket Mode crashes / reconnects in a loop | Network interruption or token revoked | `tmux kill-session -t agent-pi`, verify tokens in `.devcontainer/.env`, then relaunch | +| Socket Mode crashes / reconnects in a loop | Network interruption or token revoked | `tmux kill-session -t client-slack`, verify tokens in `.devcontainer/.env`, then relaunch | | Bot is in allow-list but channel messages ignored | Bot is not a member of the channel | In Slack, type `/invite @OpenHarness` in the target channel | ## 8. Architecture Pointer diff --git a/docs/intro.md b/docs/intro.md index cba6a90..f658b5f 100644 --- a/docs/intro.md +++ b/docs/intro.md @@ -39,7 +39,7 @@ flowchart TB subgraph sandbox["Sandbox container — default workspace"] Orch{{"Orchestrator
Claude @ project root
git · lifecycle · file edits"}} - Tmux["tmux sessions
agent-pi · agent-t3code · app-docs · system-cron"] + Tmux["tmux sessions
client-slack · agent-t3code · app-docs · system-cron"] Sock(["docker.sock"]) end