Skip to content

Repository files navigation

BoxLite Agent — @boxliteai

Mention @boxliteai in any public GitHub issue or pull request, or in your team's Slack, and it answers in the thread. For a repo's maintainers it can also open a draft PR. There's nothing to install on GitHub: it's a regular GitHub account. Each request runs Codex CLI in that thread's own BoxLite microVM, with a full shell and network, so it can run the code before it answers. Slack gets all of that too, draft PRs and the admin commands included. There it also gets the files attached to a message, and it can read your team's Linear, Notion and Google Workspace — in Slack as the person asking, so only what they can see — and create or edit Workspace documents, calendars and events, prepare Gmail drafts, and comment and file things in Linear and Notion.

@boxliteai why does `npm test` fail on this PR?                        (GitHub)
@boxliteai add retries to the client in src/http.ts and open a PR      (GitHub)
@boxliteai what broke? (ci.log attached)                               (Slack)
@boxliteai what's left on LIN-231, and does the spec in Notion agree?  (Slack)

How it works

GitHub mentions reach the controller box by polling or App push, and Slack messages over a Socket Mode connection the controller dials out. The controller holds every real credential, calls chatgpt.com as the bot and Linear, Notion and Google Workspace as the Slack requester, and runs one session box per GitHub thread or Slack requester within a thread. Session boxes hold no credentials and reach the model, the tools and (on a write turn) one staging branch of the bot's fork only through the controller. GitHub threads and Slack threads never mix: each side keeps its context on a volume of its own, and a GitHub box never mounts Slack's.

  • One box and session per GitHub thread or Slack requester within a thread. Your follow-ups resume your session; another channel member gets their own box, files and context. A PR's checkout follows its latest head; a Slack channel follow-up sees only the requester's messages since their last turn.
  • Codex may do anything in its box: no sandbox, no approval prompts, no hook reviews, sudo, network, live web search. The microVM is the boundary, and nothing worth stealing is ever inside it.
  • Every box runs agent-tooling, BoxLite's shared Codex plugin with its skills, auditors and hooks. It's refreshed to the tip of main when it's over 10 minutes old. The Codex runs its hooks start go through the controller too.
  • Only the controller holds credentials. Codex runs on a stand-in login whose token works only on the controller's proxy, and only until the turn ends.
  • GitHub and Slack never mix. See below.
  • PRs only for people who may ask. Codex commits in its box; the controller checks the change and opens the PR. Everyone else gets the change as a diff in the reply.
  • Slack events use Socket Mode. The controller dials out to Slack; browser account linking uses the controller's existing public HTTPS URL.
  • The team's tools go through the controller too. Linear, Notion and Google Workspace reach Codex as MCP servers on the controller, behind the same job token. The controller checks every call against the tools you allow and swaps in the right login — in Slack the asker's own. See Linear, Notion and Google Workspace.

One mention, start to finish

The controller picks up the mention and reacts 👀, starts the thread's box and execs the runner. The box restores its context, runs Codex with model calls through the controller, seals its context and returns the answer. The controller stops the box, then replies.

The controller stays attached to the exec for the whole turn, because BoxLite reaps an exec nobody is attached to. It stops the box as soon as the turn ends, then posts the reply. It acks every Slack event the moment it arrives. In Slack, Codex's Markdown goes out in Slack's markdown block, and a long answer is split into a few messages, never in the middle of a code block.

Memory that outlives the box

Turn 1 runs in box A and seals its context onto the volume. Box A is deleted. Turn 2 runs in a new box B, restores the context and resumes the same Codex session.

Live state (the checkout or working directory, and CODEX_HOME) stays on the box's own disk, since the S3-backed volume has no rename or append. After every turn CODEX_HOME is sealed with AES-256-GCM under a key derived for that session, and written to its directory on its side's volume: sessions/<owner>/<repo>/<n>/ for GitHub, or sessions/<team>/<channel>/<thread ts>/<requester>/ for Slack channels (DMs omit <requester>/). Every box mounts its side's whole volume, but only that session's box gets the key. A session's box is deleted after 15 quiet minutes (BOX_TTL_MIN), and the next mention restores into a new one. The conversation carries over; files from earlier turns don't, and Codex is told so.

GitHub and Slack never mix

A GitHub thread's box runs a public thread's code at anyone's request. A Slack thread holds your team's conversations. So the two sides share nothing a box can reach:

GitHub thread Slack thread
Who can ask anyone, in public repos members of your workspace
Its box botlite-gh-acme-app-7-<hash> botlite-slack-dm-alice-0922-<hash>, botlite-slack-backend-bob-0922-<hash>
Its volume botlite-context (VOLUME) botlite-slack-context (SLACK_VOLUME)
Its context key comes from CONTEXT_SECRET SLACK_CONTEXT_SECRET
The team's tools none — no shared bot login each person, on the login they linked themselves (in a DM)
Who may ask for a PR the bot's admins, the repo's maintainers, people an admin added every member, into any public repo (SLACK_PR_REPOS)
Who runs the commands the bot's admins (BOT_ADMINS) the workspace's owners and admins

A GitHub box never mounts the volume where Slack's threads are kept. Nor the other way round: BoxLite can't mount a volume read-only yet, and a Slack box that could write to GitHub's volume could leave something there for every GitHub box to read. A thread only ever runs as its own kind, and a config that would give both sides one volume or one secret keeps Slack off. A turn's job token opens only the tools that turn was given, so a stranger's turn on GitHub can't reach Linear with its own token. A box's name says which thread it runs: the repo and number, or where the Slack thread is, who started it and when. What makes it that thread's alone is the hash of the thread's whole key at its end, so nobody can name a repo to land in another thread's box. The deploy's public log shows only how the controller started, never a line about a thread.

Opening a PR

Opening a PR: an admin, a maintainer or someone an admin added asks. The controller runs the turn on the commit the change builds on. Codex commits in its box; the runner pushes with its job token to the controller, which lets one staging branch through to the bot's fork with an App token the box never sees. After the turn the controller stops the box, revokes the tokens, checks that exact commit on GitHub's own diff, squashes it into one commit by the bot, and opens a draft PR.

  • Who may ask: the bot's admins (BOT_ADMINS) anywhere; a repo's maintainers (owner, member, collaborator) there; and anyone an admin added to a repo with /add.
  • What's checked, on GitHub's diff of the exact commit pushed: that it's a change on top of the base, and nothing else. A human merges every PR, so nothing is refused for what it touches or how big it is. Called out at the top of the PR: dependency and lockfile changes, and changes to .github/workflows, .github/actions, CODEOWNERS, .gitmodules or FUNDING.yml. CI runs a PR's own workflows before anyone has reviewed it, so require approval for outside contributors' runs in a repo with self-hosted runners.
  • Its fork runs no Actions. Before a turn's first push, the controller turns Actions off on the bot's fork: a push there would run the box's code with a token that can write the fork. If it can't, nothing is pushed. The turn's push token may then carry workflow files, if the push App may write them.
  • What's published: one commit by the bot on botlite/<owner>/<repo>/<n> in its fork, as a draft PR into the default branch. For someone else's PR, the draft PR goes into that PR's branch; on a PR the bot opened, the commit goes straight onto its branch. A follow-up adds a commit, and a branch that moved meanwhile is never overwritten.

Commands

Command Who Does
@boxliteai /help (or just @boxliteai help) anyone the commands, and whether you can ask for PRs here
@boxliteai /add @user · /remove @user admins who else can ask for PRs in this repo
@boxliteai /list admins who has been added here
@boxliteai /pause · /resume admins stop or restart all PR writing
@boxliteai /model [model] [effort] admins show, or set, the model and reasoning effort every turn runs on, GitHub's and Slack's, e.g. /model gpt-6-astra xhigh; /model default undoes it
@boxliteai /deploy admins put what's merged on main live now (see below)

These are GitHub comments. In Slack, the workspace's owners and admins run the same ones on the same state: @boxliteai /model …, /deploy (it reports back in that Slack thread), /pause and /resume, so one /pause stops PR writing on both. /add, /remove and /list stay on GitHub: in Slack every member may ask for PRs.

Every Slack member can send @boxliteai /link linear, /link notion or /link google (include the mention in channels). Open the private, ten-minute link and approve your account. The browser confirms when connected; your next request uses your account. A new command replaces an unfinished link, and a controller restart expires unfinished links. Linked accounts survive restarts. /link drive, /link docs, /link sheets, /link slides, /link calendar, /link calendars, /link gmail and /link google workspace all connect the same Google account for the enabled Workspace tools.

In channels, answers are private to the person who asked, including tool results and failure notices. Alice and Bob can ask in the same thread: each uses their own linked accounts and context. One person's follow-ups run in order; different people can run concurrently within MAX_CONCURRENT. Only that person's earlier messages are imported into their channel session. Old shared channel sessions are not resumed. Private channel replies disappear after a Slack reload; use a DM for persistent replies. The bot never falls back to a public answer if private delivery fails.

/model takes a model only if ChatGPT's Codex backend offers it (and that effort) to the bot's pinned Codex, since a bad one would fail every turn.

The controller answers these itself; Codex never sees them. Admin commands count only in a new, never-edited comment, because anyone with write access to a repo can edit other people's comments there. People are kept by GitHub id, since a login can change hands.

Improving itself

The bot improving itself: asked to change its own code, it opens a draft PR from its fork, and it can't merge (it has read access). A human reviews and merges into main. An admin says /deploy: the controller lists the commits since the running build, finishes running turns and exits; the boot loop pulls main and the launcher starts the new build. A pull is checked before it starts (it must parse, pass the launcher's test and start offline): if it doesn't pass, the controller runs the newest pulled commit that does, or the build it had. Live, it says so in the thread and is on trial for 10 minutes; a build that fails three times in its trial is rolled back to the last good one, and the thread is told.

Merging to main is deploying. Once the test workflow passes on a push to main, the deploy workflow restarts the controller onto it: running turns finish first, the boot loop pulls main, and the new build starts with the bot's logins and state. /deploy does the same from a thread, and says there how it went. A bot PR that touches its own trust boundary (access, publishing, the push route, credentials, the runner, deploy) opens with a warning.

When a deploy goes wrong

If the new build… then
doesn't parse, link or start, or breaks the launcher the pull gate, a hook no pull can change, walks back to the newest pulled commit that passes (or the build it had), which starts and says why in the thread
passes the gate, but crashes 3× in its first 10 minutes, or at the end of them isn't polling GitHub or (once set up) connected to Slack the launcher rolls back to the last good build, and the thread is told
runs, but can't run a turn (a broken runner, say) a new build runs one small turn of its own a minute into its trial, and once more two minutes later if that fails; two failures roll it back
hangs, stuck or with its event loop blocked a watchdog thread kills it after 10 minutes without progress and the boot loop starts it again; a build still on trial is rolled back
can't reach GitHub or Slack /healthz answers 503; the health workflow opens an issue, and closes it once it's back
misbehaves some other way the deploy workflow's rollback runs an earlier commit of main

A rollback keeps what a newer build wrote to the state: fields an older build doesn't know are kept, not dropped.

In Slack

Mention @boxliteai in a channel it's in, or send it a direct message. It answers in the thread, and a follow-up there (a mention again, in a channel) continues the same session. Attach logs, screenshots or code to the message and the box gets them too: up to 5 MB a file and 8 MB a message. They reach that thread's box only, on the exec's stdin, and never go on a volume.

A PR from Slack. Ask for a change as a PR and it opens a draft PR from the bot's fork, into any public repo (SLACK_PR_REPOS in src/policy.mjs can narrow that, to boxlite-ai/* say). A Slack thread belongs to no repo, so the PR is asked for at the end of the turn:

  1. Codex clones the repo in its working directory, commits the change there, and leaves pr.json (which repo, which clone).
  2. The runner asks the controller for the push (/pr, with the turn's job token). The controller checks: PR writing on, the repo allowed, the commits built on its default branch, one PR per turn. Then the runner pushes to the one staging branch the controller names.
  3. After the turn, it's GitHub's path: the same checks on GitHub's own diff, one squashed commit by the bot, and a draft PR, whose link goes under the answer.

The PR is public, and says only that it was asked for in Slack. No names, links or Slack ids go into it, nor into its branch's name. Codex is told its commit messages become the PR's title and description.

Who can use it is mayUseSlack in src/policy.mjs. It runs before any quota is spent or box started, and a refused person gets an explanation only they can see. As shipped it's members only:

Who How you can tell As shipped
Full members of your workspace user.team_id === home.teamId, not a guest yes
Members of a sibling workspace on Enterprise Grid enterprise_user.enterprise_id === home.enterpriseId yes
Guests (often contractors) is_restricted; single-channel: is_ultra_restricted no
Other organizations, in Slack Connect channels team_id isn't yours, or is_stranger no
Any request in a Slack Connect channel where.extShared no: even a member's answer lands in front of the other org
Deactivated accounts, bots deleted, is_bot never (the tests insist)

Linear, Notion and Google Workspace

Each person links their own Linear, Notion and Google Workspace, and a turn uses the requester's own login — so the bot reads only what that person can already see, and no one's private data reaches anyone else through it. Someone who hasn't linked a service simply can't use it, and the bot tells them how. There is no shared bot login for anyone to borrow, and no team tools on GitHub (a public thread, run at anyone's request). In Slack, DMs use that person's account; channel requests use separate sessions per requester and private replies. Linking does not give other channel members access to your connection or results. Link only accounts that belong to you.

How a tool call flows, end to end. The box holds no tool credential. Its Codex reaches each service as an MCP server on the controller (/mcp/<service>), carrying only the turn's job token; the controller checks the call, swaps in the asker's own login, and forwards it to the service's official MCP server. Creating a separate calendar uses the same broker checks and requester token, with a small MCP adapter for Google's Calendar REST API; Google's Calendar MCP covers event tools.

sequenceDiagram
    participant A as Asker (private Slack reply)
    participant B as Session box (Codex)
    participant C as Controller (broker)
    participant S as Official MCP server
    A->>B: a request, in one thread
    Note over B: no credentials —<br/>only this turn's job token
    B->>C: POST /mcp/linear (Bearer job token)<br/>tools/call save_issue
    Note over C: verify the token · was this turn<br/>given linear? · is the login in place? ·<br/>is save_issue on the policy list? ·<br/>under the 10-change / 60-call budget?
    C->>S: the same call + the asker's own login
    S-->>C: result
    C-->>B: result (MCP headers only, no vendor cookies)
    Note over C: logs who · thread · tool,<br/>counts the change
    B-->>A: the answer, ending<br/>"changed: Linear save_issue"
Loading

Any failed check ends the call there — a service the turn wasn't given, a tool not in TOOLS, a spent budget — so a public GitHub thread can't reach a tool its turn never got, and Codex can't call one you didn't list, however it's asked. One real Slack request ("file a Linear issue and create a Notion page"), as the controller logged it:

linear list_teams                       (read: find a team)
notion notion-fetch                     (read)
linear save_issue (a change)            → the issue
notion notion-create-pages (a change)   → the page
notion notion-get-users · linear get_user · notion notion-fetch   (reads: the links)
answered Dorian, changed: Linear save_issue · Notion notion-create-pages
Linear Notion Google Workspace
What it sees what that member sees the pages shared with it files and calendars shared with it
A Slack person links their own @boxliteai /link linear @boxliteai /link notion @boxliteai /link google
Lasts until revoked; OAuth tokens refresh automatically 180 days, then link again (ctl status shows the date) until revoked, with an Internal consent screen

Linking a Slack person binds their own token, keyed to their Slack id — the U… in the log's request from … (U…). The person completes any browser consent themselves, so it's their access being bound, and the operator never sees a Notion/Google token. node deploy/ctl.mjs links lists who's bound; node deploy/ctl.mjs unlink <service> <id> removes one. When someone asks for a tool they haven't linked, the bot tells them to link it first and does nothing else with it.

  • Linear: browser linking uses Linear MCP's OAuth registration and PKCE, bound to the requester and browser. Tokens stay on the controller; its tool allowlist still limits changes. No OAuth app setup is needed. Existing personal API keys remain supported through LINEAR_API_KEY=… node deploy/ctl.mjs link linear <their Slack id>.
  • Notion: the private Slack link uses Notion MCP's OAuth registration and PKCE. The person approves it as themselves; nothing to set up first. Terminal linking remains available through node deploy/ctl.mjs link notion <their Slack id>.
  • Google: the Workspace MCP servers are in a Developer Preview. Join it, then in a Cloud project enable the Drive, Docs, Sheets, Slides, Calendar and Gmail APIs and their MCP APIs, and set the OAuth consent screen to Internal. For Slack linking, create a Web application OAuth client with the exact authorized redirect URI https://<controller-public-host>/link/google/callback. Hand it to the controller once: GOOGLE_WEB_CLIENT_ID=… GOOGLE_WEB_CLIENT_SECRET=… node deploy/ctl.mjs google-client. The client stays on the controller, takes effect without restarting, and grants no user access by itself: each person chooses their account and approves consent. Missing setup gives a private administrator-setup message. Scopes come from the tool policy; link again after allowing new tools. Terminal linking still uses a separate Desktop app client through GOOGLE_CLIENT_ID=… GOOGLE_CLIENT_SECRET=… node deploy/ctl.mjs link google <their Slack id>.

Terminal logins run on your machine and hand tokens to the controller. Slack's browser linking exchanges the authorization code on the controller; neither Slack nor a session box gets tokens.

What the bot may do is TOOLS in src/policy.mjs: reads are listed, and every other call is refused before it reaches the service, whatever Codex asks. Changes are the ones in WRITES: as shipped, comments and issues in Linear (save_issue edits issues too) and comments and new pages in Notion, Workspace file creation and editing, calendar creation and event invitations, and Gmail drafts. File edits can replace existing content, and event invitations notify their attendees. A change is made as the person asking, on their own login, in their private session with the bot. In channels, only that person's messages enter the session. The enabled Google changes are:

Service Change tools Notes
Drive create_file, copy_file Create Docs, Sheets and Slides with their Google MIME type
Docs · Sheets update_doc · update_values, update_formulas, update_spreadsheet, insert_dimension Edit files the requester can edit
Slides update_presentation Edits may replace or remove presentation content
Calendar create_event, update_event Invite the requested attendee addresses; use notificationLevel: ALL to notify them
Calendars create_calendar Create a separate calendar owned by the requester; does not share it
Gmail create_draft Prepare mail for the requester to review and send themselves

For example: “Create a Planning calendar, schedule a kickoff with alice@polygala.ai, and draft an email about it.” The calendar id from calendars.create_calendar becomes the event's calendarId; gmail.create_draft leaves the email in Drafts. For “everyone,” provide a verified group address or attendee list: the bot has no Workspace directory access to enumerate members.

Google's gmail.compose scope also permits sending, but the controller exposes only draft creation and refuses send tools. Calendar creation uses calendar.app.created; event writes use calendar.events. The controller exposes no calendar deletion, calendar sharing or mail sending tools. After tools/scopes change, each member must run /link google again to approve access.

Each request may make up to 10 changes in up to 60 tool calls. Under its answer, the bot lists the changes it made, as the controller recorded them. The controller log records every tool call: who asked, in which thread, which tool.

What stays risky: what Codex reads through these tools sits in a box with open internet access. The controller keeps the logins out of the box and limits what Codex can do, but a message that talks Codex into it could still send what it read somewhere else. On GitHub, that includes the public reply. Keep the bot's accounts narrow, and add changes one at a time.

Who holds what

Credential Controller Session box Volume
Bot's GitHub token holds: polls, reacts, replies, forks, opens PRs never (clones anonymously) never
Push App key holds: one token per write turn, for that fork only never (pushes via the controller) never
Slack bot token (xoxb-) holds: reads threads, people, files; reacts, replies never never
Slack app token (xapp-) holds: opens Socket Mode connections never never
Linear API key holds: Linear's MCP server never never
Notion and Google logins hold and refresh them never never
BoxLite API key placeholder (a BoxLite secret) never never
ChatGPT login holds and refreshes never (a stand-in login) never
Job token issues, revokes its own turn only: model calls, a write turn's one push (a Slack turn asks for it at /pr), the tools its turn was given never
Thread context key derives, from its side's secret its own thread only never (sealed bytes only)
Webhook secret holds never never

The proxy forwards only Codex's two model endpoints, a write turn's push to its one staging branch and the tool calls your policy allows the turn. It 404s everything else, and caps requests and tool calls per turn. Text from GitHub and Slack goes into the prompt fenced as untrusted context, never as instructions.

Every key the controller holds is also in the repo's production environment (see From GitHub), so the controller box isn't the only copy. The ChatGPT, Notion and Google logins are the exception: their refresh tokens change as they're used, so a copy anywhere else would go stale.

Code map

File Runs in Does
src/main.mjs controller the launcher: rolls back a build that won't go live, then starts the controller
src/controller.mjs controller wiring: credentials, poll loop, quotas, replies, drain on restart
src/deploy.mjs controller /deploy: what's merged since the running build, and how the deploy went
src/mentions.mjs · webhook.mjs controller mentions from polled notifications or App pushes
src/slack-channel.mjs controller Slack: who may ask, each message to a turn, the answer back
src/slack-socket.mjs · slack-events.mjs controller Socket Mode events, acked at once; Slack events → requests, markup → text, files
src/slack.mjs · slack-reply.mjs controller Slack Web API as the bot: 👀, replies, file downloads
src/policy.mjs controller your policy: who may use the bot in Slack, which tools it may use
src/tools.mjs · oauth.mjs controller the tool broker at /mcp/<service>; the bot's logins, kept fresh
src/jobs.mjs · state.mjs controller one turn per session, a few at once; seen requests, sessions, quotas
src/session.mjs controller one turn: its side, start or create the box, exec the runner attached, stop it
src/proxy.mjs · chatgpt.mjs controller the public port: model endpoints only, job token → real login
src/codex.mjs controller codex exec / resume arguments, prompts, event parsing
src/access.mjs controller who may publish on GitHub; /help and the admin commands
src/gitpush.mjs · githubapp.mjs controller a write turn's one push: job token in, App token out, one ref
src/prgrant.mjs controller /pr: a Slack turn asks for its PR's push when its work is done
src/publish.mjs controller plan a write turn; check the pushed commit, squash it, draft PR
src/boxlite.mjs controller BoxLite REST, exec attach over WebSocket
src/github.mjs · reply.mjs controller GitHub REST as the bot: 👀 and replies
box/session.mjs session box restore → stand-in login → checkout or files → Codex → push commits → seal
deploy/deploy.sh · ctl.mjs your terminal, GitHub create the controller; operate it, hand over keys, roll back
deploy/login.mjs your terminal the one-time Notion and Google consent, handed to the controller
deploy/post-merge.sh controller the pull gate: a pull runs only up to its newest commit that passes
.github/workflows/ GitHub Actions test every PR; deploy main when its tests pass; health
slack/manifest.json Slack the app: bot scopes, events, Socket Mode on

Run your own

export BOXLITE_API_KEY=blk_live_…
BOT_ADMINS=you bash deploy/deploy.sh                  # creates the public botlite-controller box
GITHUB_TOKEN=ghp_… node deploy/ctl.mjs github-token   # the bot account's classic PAT
GITHUB_APP_ID=… GITHUB_APP_KEY=app.pem node deploy/ctl.mjs github-app   # optional: PR writing
SLACK_BOT_TOKEN=xoxb-… SLACK_APP_TOKEN=xapp-… node deploy/ctl.mjs slack-tokens   # optional: Slack
node deploy/ctl.mjs status                            # what it's waiting for, e.g. the ChatGPT login
  • BoxLite key: it must be able to create boxes. If it can't create volumes, create botlite-context and botlite-slack-context in the dashboard first.
  • GitHub token: a classic PAT on the bot's own account with notifications + repo + workflow (the notifications API rejects fine-grained tokens). workflow lets the bot's forks catch up with an upstream that changed a workflow; without it, PRs from such a fork fail. repo lets the bot turn Actions off on its forks, which PR writing needs (with public_repo alone it only answers). It also reaches private repos, so keep the bot's account out of them: the deploy warns if it can see any. Never delete_repo. The bot is whoever the token belongs to.
  • Push App (PR writing): a GitHub App of its own, separate from the webhook App. Give it Repository permissions → Contents: Read and write, and Workflows: Read and write if its PRs may change CI, with no webhook. Install it on the bot's account for all repositories, so new forks are covered, generate a private key, and hand both over with ctl github-app. Without it the bot only answers. The controller mints one token per write turn for that turn's fork, and the box never sees it.
  • Slack app: at api.slack.com/apps, Create New App → From a manifest, pick the workspace and paste slack/manifest.json. Under Basic Information → App-Level Tokens, generate one with the connections:write scope: that's the xapp-… token. Install App → Install to Workspace gives the Bot User OAuth Token, xoxb-…. Optionally, upload slack/icon.png as the app icon. Invite the bot where people should use it: /invite @boxliteai. No restart needed: the controller connects once the tokens are in. The manifest leaves out channels:read and groups:read; add them if you want channel threads' boxes named after their channel, not only after who started them.
  • ChatGPT login: the controller runs codex login --device-auth in its box, and status shows the link and code to approve with the bot's ChatGPT account. Use an account only the bot uses, and never copy another controller's auth.json: each refresh rotates the token, so two holders of one login knock each other out.
  • CONTEXT_SECRET seals GitHub threads' contexts and signs job tokens, and SLACK_CONTEXT_SECRET seals Slack threads'. Each is generated on first start. Pass the same values again to keep every thread's context: CONTEXT_SECRET=… bash deploy/deploy.sh, and SLACK_CONTEXT_SECRET=… node deploy/ctl.mjs slack-tokens with the Slack tokens.

Day to day: node deploy/ctl.mjs status | logs [n] | webhook | restart | rollback <commit> | admins <logins>. restart pulls the tracked branch and lets running turns finish first (Slack requests that arrive meanwhile are kept for the next start); rollback runs an earlier commit of it until the next restart; admins replaces BOT_ADMINS and restarts, with no redeploy. Add --wait to any of the three to wait until the next start is live and see which build it is.

From GitHub

Keep the keys in the repo's production environment instead of on a laptop, with only main allowed to deploy:

  • secrets BOXLITE_API_KEY, BOT_GITHUB_TOKEN, PUSH_APP_PRIVATE_KEY, SLACK_BOT_TOKEN, SLACK_APP_TOKEN, SLACK_CONTEXT_SECRET and, if the bot uses Linear, LINEAR_API_KEY;
  • CONTEXT_SECRET and WEBHOOK_SECRET, for recreating the controller box with deploy.sh;
  • variables PUSH_APP_ID and BOT_ADMINS.

Then the deploy workflow operates the controller. Every push to main whose tests pass goes live by itself; by hand it does:

Action Does
restart pull main and restart onto it
handover give the controller its tokens and keys from the environment, e.g. after a rotation, and reinstall the pull gate
rollback run an earlier commit of main until the next deploy

Each run waits until that build is live, and fails with how its start went if it isn't. That log is public, so it shows only the lines about starting. health checks /healthz every 15 minutes once the CONTROLLER_URL variable is set. test runs npm test on every PR.

Settings

The controller reads these from its environment; deploy.sh passes VOLUME, CODEX_MODEL, CODEX_EFFORT, BOTLITE_REF, BOT_ADMINS, CONTEXT_SECRET and WEBHOOK_SECRET through.

Env Default
BOT_ADMINS none GitHub logins, comma-separated, who may ask for PRs anywhere, use the team's tools on GitHub, and run the admin commands (ctl admins replaces it)
VOLUME / SLACK_VOLUME botlite-context / botlite-slack-context GitHub threads' and Slack threads' context volumes, never the same one
CODEX_MODEL Codex's default model for every turn (also pinned by the proxy), until an admin's /model
CODEX_EFFORT the model's default reasoning effort for every turn (low … xhigh, max, ultra, as the model allows), until /model
BOTLITE_REF main the branch the controller runs
SESSION_IMAGE / SESSION_CPUS / SESSION_MEMORY_MIB node / 2 / 4096 session boxes
MAX_CONCURRENT 3 turns running at once
DAILY_LIMIT_PER_USER 20 requests per GitHub user per UTC day; the bot's admins have no limit
SLACK_DAILY_LIMIT 0 (no limit) requests per Slack user per UTC day
JOB_TIMEOUT_MIN 20 wall-clock limit of one turn
BOX_TTL_MIN 15 a thread's box is deleted after this many minutes without a turn (BoxLite's auto_delete; there's no auto-stop, since the controller stops each box after its turn)
MAX_FILE_MB / MAX_FILES_MB 5 / 8 the largest Slack attachment the box gets / all of a message's together
HANG_MIN 10 the watchdog kills a controller that makes no progress this long
PORT / PUBLIC_URL 8788 / looked up the proxy's port and public origin
BOXLITE_URL https://api.boxlite.ai BoxLite API

Instant pickup (optional)

Polling takes about 30–85 s from mention to pickup. Repos that install the BoxLite Agent GitHub App get each mention pushed to the controller's POST /webhook instead. node deploy/ctl.mjs webhook prints the URL and secret for the App. Subscribe it to Issues, Issue comment, Pull request and Pull request review comment, with read-only Issues and Pull requests permissions. A mention that arrives both ways is handled once.

Develop

Install the shared agent-tooling once per clone or worktree (requires Git, Bash, jq, and Perl):

./.agent-tooling/install.sh

This configures local Git hooks and refreshes the shared guidance in AGENTS.md. The repository profile in .agent-tooling/profile.json follows tooling main and declares npm test. Host plugins require installation in the host.

npm test                 # offline: no network, BoxLite, Slack or model
BOTLITE_E2E=1 npm test   # + a real Codex turn and resume through the proxy (needs codex 0.155.1)

Updating agent tooling

Run ./.agent-tooling/install.sh in each worktree after a tooling upgrade to refresh hooks and managed guidance. Keep committed bootstrap scripts synchronized with the adopted release templates.

The 0.1.26 Claude bootstrap checks the adopted manifest version and reinstalls a stale project plugin when an update leaves it unchanged, preserving project settings. Run /reload-plugins afterward to activate the refreshed plugin.

Limits

  • Public repos only, and PRs only on request. It opens draft PRs from its own fork, only for the people above, and never pushes to anyone else's branch.
  • Forks that fall behind. Before a push the controller syncs the fork with upstream. If upstream changed a workflow file since the last sync, GitHub refuses that sync unless the bot's PAT has the workflow scope, and then refuses the push, which would bring that change in. The reply says which workflow and what the operator can do.
  • One Slack workspace, answers only. It's an internal app on Socket Mode; offering it to other workspaces would take OAuth and the Events API. It sees only its thread, and posts only there.
  • The box has the open internet. A Slack thread's text, private channels included, and whatever the tools read go into a machine that can send them anywhere if a message talks Codex into it. Keep the bot out of channels whose contents must not leave.
  • A personal ChatGPT plan serves everyone. OpenAI's terms may not allow a consumer login to be used this way; an API key is the sanctioned route for a public service.
  • Codex's private backend. The model path depends on ChatGPT's Codex backend and Codex's login format as of 0.155.1, which is pinned. BOTLITE_E2E=1 npm test checks it on upgrade. The backend offers each model only from some Codex version on (gpt-6-astra: 0.153.0).
  • Tool names come from the services. The allowed tools are listed by name. A service that renames a tool turns it off until the list is updated; the controller log names every refused call.
  • Region. The controller must reach chatgpt.com and auth.openai.com from a country OpenAI supports.

About

GitHub agent that reviews PRs with Claude inside an isolated BoxLite microVM — a showcase of BoxLite managed agents.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages