-
Notifications
You must be signed in to change notification settings - Fork 2
Add a Slack connector #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
617a410
eca5ecf
a534fce
e54a3bb
759524f
cbd4f18
ad2c325
3cf3f06
be7949a
9da8e62
6152f50
c3176ee
58af771
15fc844
2323320
981fb80
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| .git | ||
| node_modules | ||
| dist | ||
| .telex | ||
| .env | ||
| *.log |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,34 @@ | ||
| # Telex container image. The container is the isolation boundary: the process | ||
| # runs as the unprivileged `telex` user and all state lives under /data. | ||
| FROM node:24-slim AS build | ||
| WORKDIR /app | ||
| COPY package.json package-lock.json ./ | ||
| RUN npm ci --no-audit --no-fund | ||
| COPY . . | ||
| RUN npm run build && npm prune --omit=dev | ||
|
|
||
| FROM node:24-slim | ||
| ARG TELEX_UID=1001 | ||
| RUN apt-get update \ | ||
| && apt-get install -y --no-install-recommends ca-certificates curl git ripgrep \ | ||
| && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg \ | ||
| -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ | ||
| && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ | ||
| > /etc/apt/sources.list.d/github-cli.list \ | ||
| && apt-get update \ | ||
| && apt-get install -y --no-install-recommends gh \ | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| && rm -rf /var/lib/apt/lists/* \ | ||
| && useradd --create-home --uid "${TELEX_UID}" --user-group telex | ||
| WORKDIR /app | ||
| COPY --from=build /app/node_modules ./node_modules | ||
| COPY --from=build /app/dist ./dist | ||
| COPY --from=build /app/package.json /app/codex.version ./ | ||
| COPY docker/entrypoint.sh /usr/local/bin/telex-entrypoint | ||
| RUN chmod 0755 /usr/local/bin/telex-entrypoint | ||
| ENV TELEX_DATA_DIR=/data/telex \ | ||
| CODEX_WORKSPACE=/data/workspace | ||
| VOLUME /data | ||
| # The entrypoint starts as root only to take ownership of freshly created | ||
| # volumes, then drops to the unprivileged telex user before running Telex. | ||
| ENTRYPOINT ["telex-entrypoint"] | ||
| CMD ["node", "dist/index.js"] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| # Example Compose deployment. Copy next to your .env (SLACK_* and/or | ||
| # TELEGRAM_* variables) and run: docker compose up -d --build | ||
| # | ||
| # State lives in the `telex-data` volume. To keep it on the host under a | ||
| # dedicated user instead, replace the volume with a bind mount owned by | ||
| # uid 1001 (the in-container `telex` user): | ||
| # volumes: | ||
| # - /srv/telex/data:/data | ||
| services: | ||
| telex: | ||
| build: .. | ||
| env_file: .env | ||
| init: true | ||
| restart: unless-stopped | ||
| volumes: | ||
| - telex-data:/data | ||
|
|
||
| volumes: | ||
| telex-data: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| #!/bin/sh | ||
| # Seed the Codex configuration on a fresh volume. Codex's own Linux sandbox | ||
| # needs user namespaces, which Docker's default seccomp/AppArmor confinement | ||
| # blocks; the container itself is the isolation boundary instead. | ||
| set -e | ||
|
|
||
| data_dir="${TELEX_DATA_DIR:-/data/telex}" | ||
| workspace="${CODEX_WORKSPACE:-/data/workspace}" | ||
|
|
||
| # Fresh named volumes are created owned by root; take ownership of the state | ||
| # roots (non-recursively), then continue as the unprivileged telex user. | ||
| if [ "$(id -u)" = "0" ]; then | ||
| mkdir -p "${data_dir}" "${workspace}" | ||
| chown telex:telex /data "${data_dir}" "${workspace}" 2>/dev/null || true | ||
| exec env HOME=/home/telex runuser -u telex -- "$0" "$@" | ||
| fi | ||
|
|
||
| mkdir -p "${data_dir}/codex-home" "${workspace}" | ||
|
Comment on lines
+10
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- docker/entrypoint.sh ---'
cat -n docker/entrypoint.sh
printf '%s\n' '--- related Docker configuration ---'
rg -n -C 3 'entrypoint|data_dir|workspace|codex-home|/data|/workspace|runuser|USER ' Dockerfile* docker compose*.yml compose*.yml 2>/dev/null || true
printf '%s\n' '--- references to state paths ---'
rg -n -C 2 'codex-home|workspace|data_dir|HOME=/home/telex|/data' . --glob '!node_modules' --glob '!dist' --glob '!build'Repository: sadfun/telex Length of output: 50371 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Dockerfile ---'
cat -n Dockerfile
printf '%s\n' '--- docker/docs state and ownership guidance ---'
cat -n docs/docker.md
printf '%s\n' '--- application directory creation and persistence writes ---'
rg -n -C 4 'ensureDirectory|writeFile|appendFile|mkdir\(|rm\(|rename\(|dataDirectory|workspace|codexHome|outbound|conversations|automations' src/index.ts src --glob '*.ts' | head -n 500
printf '%s\n' '--- entrypoint behavior model ---'
python3 - <<'PY'
import os
import pathlib
import tempfile
with tempfile.TemporaryDirectory() as root:
data = pathlib.Path(root) / "data"
state = data / "telex"
workspace = data / "workspace"
nested = state / "codex-home" / "config.toml"
nested.parent.mkdir(parents=True)
workspace.mkdir(parents=True)
nested.write_text("existing")
os.chown(data, 0, 0)
os.chown(state, 0, 0)
os.chown(workspace, 0, 0)
os.chown(nested.parent, 0, 0)
os.chown(nested, 0, 0)
# This models chown telex:telex /data "$data_dir" "$workspace":
# only the named paths change; descendants do not.
target_uid = target_gid = os.getuid()
for path in (data, state, workspace):
os.chown(path, target_uid, target_gid)
print("data owner:", (data.stat().st_uid, data.stat().st_gid))
print("state owner:", (state.stat().st_uid, state.stat().st_gid))
print("workspace owner:", (workspace.stat().st_uid, workspace.stat().st_gid))
print("codex-home owner:", (nested.parent.stat().st_uid, nested.parent.stat().st_gid))
print("config owner:", (nested.stat().st_uid, nested.stat().st_gid))
PYRepository: sadfun/telex Length of output: 34700 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- relevant application startup and filesystem helpers ---'
cat -n src/index.ts | sed -n '45,115p'
cat -n src/shared/fs.ts
printf '%s\n' '--- persistent store write paths ---'
rg -n -C 5 'atomicWrite|writeFile|rename|mkdir' src --glob '*store*.ts' --glob 'src/automations/*.ts' --glob 'src/shared/fs.ts'
printf '%s\n' '--- deterministic entrypoint checks ---'
python3 - <<'PY'
from pathlib import Path
import re
text = Path("docker/entrypoint.sh").read_text()
match = re.search(r'^\s*chown\s+(.+?)\s+2>/dev/null\s+\|\|\s+true\s*$', text, re.M)
assert match, "The suppressed chown command was not found"
command = match.group(0)
print("suppressed chown:", command)
print("recursive flag present:", "-R" in match.group(1).split())
print("failure is ignored:", "|| true" in command)
print("runuser follows chown:", text.index("runuser") > text.index("chown"))
print("post-drop mkdir follows runuser:", text.index('mkdir -p "${data_dir}/codex-home"') > text.index("runuser"))
PYRepository: sadfun/telex Length of output: 12309 Fail before dropping privileges when ownership setup is incomplete. Line 14 ignores Fail when 🤖 Prompt for AI Agents |
||
|
|
||
| config="${data_dir}/codex-home/config.toml" | ||
| if [ ! -f "${config}" ]; then | ||
| cat > "${config}" <<'EOF' | ||
| # Managed by Telex. You can edit this file. | ||
| # The container provides isolation; Codex's Linux sandbox is unavailable here. | ||
| approval_policy = "on-request" | ||
| sandbox_mode = "danger-full-access" | ||
| web_search = "live" | ||
| cli_auth_credentials_store = "file" | ||
| project_root_markers = [] | ||
| EOF | ||
| fi | ||
|
|
||
| # With GH_TOKEN set, let git clone/fetch over HTTPS through gh's credential | ||
| # helper (gh itself reads GH_TOKEN directly). | ||
| if [ -n "${GH_TOKEN:-}" ] && command -v gh >/dev/null 2>&1; then | ||
| gh auth setup-git >/dev/null 2>&1 || true | ||
| fi | ||
|
|
||
| exec "$@" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # Running Telex in Docker | ||
|
|
||
| The image runs Telex as the unprivileged `telex` user (uid 1001) with all | ||
| state — Codex home (auth, config, threads), the pinned Codex toolchain, the | ||
| workspace, and conversation/automation stores — under the `/data` volume. | ||
| No ports need to be published: both the Telegram and Slack connectors dial | ||
| out (long polling / Socket Mode). | ||
|
|
||
| ```bash | ||
| cp docker/docker-compose.example.yml docker-compose.yml | ||
| cp .env.example .env # fill in SLACK_* and/or TELEGRAM_* variables | ||
| docker compose up -d --build | ||
| docker compose logs -f | ||
| ``` | ||
|
|
||
| To keep state on the host under a dedicated user instead of a named volume: | ||
|
|
||
| ```bash | ||
| useradd --system --uid 1001 --user-group --shell /usr/sbin/nologin telex | ||
| mkdir -p /srv/telex/data && chown -R telex:telex /srv/telex/data | ||
| # then bind-mount /srv/telex/data:/data in the compose file | ||
| ``` | ||
|
|
||
| ## Codex sandboxing inside the container | ||
|
|
||
| On bare Linux, Codex sandboxes shell commands with a bubblewrap helper that | ||
| needs unprivileged user namespaces. Docker's default seccomp and AppArmor | ||
| confinement blocks that, so the entrypoint seeds `config.toml` with | ||
| `sandbox_mode = "danger-full-access"` on a fresh volume: the container — an | ||
| isolated filesystem, an unprivileged user, and no host mounts beyond `/data` | ||
| — is the sandbox boundary instead. Keep that in mind before bind-mounting | ||
| anything sensitive into the container. | ||
|
|
||
| ## Notes | ||
|
|
||
| - The settings Mini App binds to `HOST:PORT` inside the container; publish | ||
| the port and set `PUBLIC_URL` if you use it with the Telegram connector. | ||
| - Telex's release self-update (`/update`, `TELEX_UPDATE_MODE=auto`) does not | ||
| apply to containers — rebuild the image to update instead. | ||
| - The first start on a fresh volume downloads the pinned Codex CLI from npm | ||
| into `/data/telex/toolchains`. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,177 @@ | ||
| # Slack connector | ||
|
|
||
| Telex can bridge Codex into Slack alongside Telegram. The connector uses | ||
| [Socket Mode](https://docs.slack.dev/apis/events-api/using-socket-mode), so it | ||
| needs no public URL, webhook endpoint, or reverse proxy — the bridge dials out | ||
| to Slack exactly like the Telegram long-polling connection. | ||
|
|
||
| What works in Slack: | ||
|
|
||
| - Direct messages with the bot: send a message, watch live progress, get the | ||
| final answer, exchange file attachments. | ||
| - Channels and group DMs: mention the bot (`@Telex fix the build`) and it | ||
| answers in a thread. Every message addressed to the bot needs a mention — | ||
| including follow-ups in the same thread — so human discussion around it | ||
| stays untouched. Each thread is its own Codex conversation with persistent | ||
| context. When first mentioned inside an existing thread, the bot reads the | ||
| earlier thread messages (up to 100, newest-biased) as context, so it | ||
| understands the discussion it was called into. | ||
| - Approvals: when Codex asks for confirmation, the question arrives as Slack | ||
| buttons. | ||
| - Scheduled runs: results are delivered to the channel or thread that created | ||
| them, with a Continue button. | ||
| - Commands: `/telex new`, `/telex status`, and friends (Slack reserves plain | ||
| `/new`-style messages for its own slash commands, so Telex registers a single | ||
| `/telex` command with subcommands). | ||
|
|
||
| When Codex creates a report, archive, image, or another deliverable, Telex can | ||
| upload it into the same Slack DM or thread. The connector-aware system context | ||
| instructs Codex to link the workspace-local deliverable in its final answer; | ||
| Telex validates and snapshots that file before uploading it with Slack's | ||
| `files:write` permission. Local links used only as code references are not | ||
| uploaded. | ||
|
|
||
| The settings Mini App remains Telegram-only because it authenticates through | ||
| Telegram. Everything else — including `/telex login` for the ChatGPT sign-in — | ||
| works from Slack. | ||
|
|
||
| ## 1. Create the Slack app | ||
|
|
||
| 1. Open <https://api.slack.com/apps> and click **Create New App**. | ||
| 2. Choose **From a manifest**, pick your workspace, and paste the manifest | ||
| below (YAML tab). Rename the app if you like — the name is what you will | ||
| @mention. | ||
| 3. Click **Create**. | ||
|
|
||
| ```yaml | ||
| display_information: | ||
| name: Telex | ||
| description: Codex in your Slack | ||
| background_color: "#1a1d21" | ||
| features: | ||
| app_home: | ||
| messages_tab_enabled: true | ||
| messages_tab_read_only_enabled: false | ||
| bot_user: | ||
| display_name: Telex | ||
| always_online: true | ||
| slash_commands: | ||
| - command: /telex | ||
| description: Control Telex (new, stop, status, help…) | ||
| usage_hint: "new | back | stop | status | help" | ||
| should_escape: false | ||
| oauth_config: | ||
| scopes: | ||
| bot: | ||
| - chat:write | ||
| - im:history | ||
| - channels:history | ||
| - groups:history | ||
| - mpim:history | ||
| - files:read | ||
| - files:write | ||
| - users:read | ||
| - commands | ||
| settings: | ||
| event_subscriptions: | ||
| bot_events: | ||
| - message.channels | ||
| - message.groups | ||
| - message.im | ||
| - message.mpim | ||
| interactivity: | ||
| is_enabled: true | ||
| org_deploy_enabled: false | ||
| socket_mode_enabled: true | ||
| token_rotation_enabled: false | ||
| ``` | ||
|
|
||
| ## 2. Collect the two tokens | ||
|
|
||
| - **App-level token** (`xapp-…`): in the app's **Basic Information** page, | ||
| scroll to **App-Level Tokens**, click **Generate Token and Scopes**, name it | ||
| (for example `telex-socket`), add the `connections:write` scope, and | ||
| generate. Copy the `xapp-…` value — this is `SLACK_APP_TOKEN`. | ||
| - **Bot token** (`xoxb-…`): open **Install App** (or **OAuth & Permissions**), | ||
| click **Install to Workspace**, and approve. Copy the **Bot User OAuth | ||
| Token** — this is `SLACK_BOT_TOKEN`. | ||
|
|
||
| ## 3. Decide who is allowed | ||
|
|
||
| Telex answers only authorized users. Two modes: | ||
|
|
||
| - **Allowlist**: comma-separated member IDs. In Slack, open a profile → | ||
| **⋯ (More)** → **Copy member ID**; it looks like `U0123ABCDEF`. | ||
| - **Whole workspace**: `SLACK_ALLOWED_USER_IDS=*` authorizes every regular | ||
| member of the workspace the app is installed in. Bots, deactivated | ||
| accounts, single/multi-channel guests, and Slack Connect participants from | ||
| other workspaces are still rejected (membership is verified through | ||
| `users.info` and cached for ten minutes, so deactivating someone in Slack | ||
| locks them out without a restart). | ||
|
|
||
| Everyone shares one Telex: the same Codex account, the same workspace | ||
| directory on the host, and the same conversation state per channel/thread. | ||
| Open it to the whole workspace only if that is acceptable. | ||
|
|
||
| Optionally, `SLACK_ADMIN_USER_IDS` (comma-separated member IDs) restricts | ||
| instance-wide commands — `/telex config`, `login`, `logout`, `reload`, | ||
| `restart`, `update` — to the listed users. Unset, every authorized user may | ||
| run them. `/telex config` opens interactive Codex settings built from Slack | ||
| buttons (model, reasoning effort, speed tier, approvals, sandbox, web | ||
| search) in the bot DM — the Slack counterpart of the Telegram Mini App. | ||
|
|
||
| ## 4. Configure Telex | ||
|
|
||
| Add the three variables to the environment (`.env` for a source checkout, or | ||
| `~/.config/telex/telex.env` for an installed release): | ||
|
|
||
| ```dotenv | ||
| SLACK_BOT_TOKEN=xoxb-… | ||
| SLACK_APP_TOKEN=xapp-… | ||
| SLACK_ALLOWED_USER_IDS=U0123ABCDEF,U0456GHIJKL | ||
| ``` | ||
|
|
||
| All three must be set together; leaving them all unset keeps the connector | ||
| disabled. Telegram is optional when Slack is configured — with only the Slack | ||
| variables set, Telex runs Slack-only (the Telegram bot and the settings Mini | ||
| App stay off). Restart Telex and check the log for | ||
| `Slack bot connected through Socket Mode`. | ||
|
|
||
| ## 5. Talk to it | ||
|
|
||
| - **Direct message**: open the app under **Apps** in the Slack sidebar and | ||
| send a message. If Slack says the app cannot receive messages, enable the | ||
| Messages Tab: app settings → **App Home** → check *Allow users to send Slash | ||
| commands and messages from the messages tab* (the manifest above enables it, | ||
| but workspaces occasionally need a re-toggle), then reload Slack. | ||
| - **Channel**: invite the bot (`/invite @Telex`), then mention it: | ||
| `@Telex what does this repo do?`. The reply opens a thread; address it | ||
| there with a mention each time (`@Telex and now check the tests`) — the | ||
| thread's Codex conversation continues across mentions. | ||
| - **Commands**: `/telex help` anywhere, or prefix a command in a mention: | ||
| `@Telex /new`. In the bot DM, plain `/new` will not reach Telex — Slack | ||
| intercepts everything that starts with `/` — so use `/telex new`. | ||
| Conversation-scoped commands (`new`, `back`, `stop`, `schedules`, | ||
| `continue`) only work as `/telex …` in the bot DM; in a channel each thread | ||
| is its own conversation, so run them inside the thread as a mention | ||
| (`@Telex /stop`). | ||
| - **Sign-in**: if Codex is not signed in yet, `/telex login` in the bot DM | ||
| returns the ChatGPT device-code link, exactly like `/login` on Telegram. | ||
|
|
||
| ## Notes and limits | ||
|
|
||
| - **Authorization**: messages, commands, and button clicks from users outside | ||
| `SLACK_ALLOWED_USER_IDS` are ignored (and logged). Scheduled runs re-check | ||
| the owner against the allowlist before every unattended execution. | ||
| - **Thread context after a restart**: the "already read this thread" memory | ||
| is in-process, so the first mention after a Telex restart re-reads the | ||
| thread history. The Codex conversation itself is persisted and continues. | ||
| - **Attachments**: inbound files are downloaded through Slack's private file | ||
| URLs with the bot token (never sent to third-party hosts); generated files | ||
| are uploaded back with `files.uploadV2`. Slack voice clips are transcribed | ||
| the same way Telegram voice messages are. | ||
| - **Formatting**: Codex's Markdown is converted to Slack mrkdwn (headings | ||
| become bold lines, `**bold**` becomes `*bold*`, links become | ||
| `<url|label>`); code blocks pass through untouched. | ||
| - **Rate limits**: live progress is streamed by editing a single message at | ||
| most every 1.5 seconds, which stays inside Slack's `chat.update` budget. |
Uh oh!
There was an error while loading. Please reload this page.