An event-driven AI team member that watches Slack and scheduled jobs, resumes OpenCode sessions through the runner, and reaches external systems through remote-cli.
ingress -> gateway -> runner -> opencode -> codex-lb -> ChatGPT
\
-> remote-cli -> MCP upstreams / CLI integrations
gatewayaccepts Slack, GitHub webhook, and cron events, batches them, and forwards them to the runner.runnermanages OpenCode session continuity and Slack progress updates.remote-cliexposesPOST /exec/*endpoints for git, gh, sandbox, scoutqa, metabase, MCP tool calls (Atlassian, Grafana, PostHog, Langfuse), direct Slack approval-card posting, and approval status/resolution.codex-lbis an OpenAI-compatible proxy that fronts ChatGPT for opencode, pooling one or more ChatGPT account credentials so no paid OpenAI API key is needed. Its account/quota dashboard sits behind the same SSO + admin-email gate as/admin/.
| Service | Port | Package | Role |
|---|---|---|---|
codex-lb |
2455 | Docker image | ChatGPT-backed OpenAI-compatible proxy |
cron |
- | docker/cron |
Scheduled prompts |
mitmproxy |
3080 | docker/mitmproxy |
Explicit outbound HTTP(S) proxy |
gateway |
3002 | @thor/gateway |
Slack/GitHub webhook ingestion and batching |
remote-cli |
3004 | @thor/remote-cli |
CLI + MCP policy gateway |
admin |
3005 | @thor/admin |
Admin dashboard and workspace configuration |
ingress |
8080 | docker/ingress |
Reverse proxy + Vouch integration |
opencode |
4096 | Docker image | Headless agent runtime |
runner |
3000 | @thor/runner |
Session lifecycle + Slack progress updates |
vouch |
9090 | Docker image | OAuth/SSO proxy |
- Copy
.env.exampleto.envand fill in the required secrets. Per-integration env vars are documented in each integration's doc (see Integrations below). - Initialize the mitmproxy CA on the host:
./scripts/mitmproxy-ca-init.shAll outbound HTTP(S) from OpenCode is routed through mitmproxy; see docs/feat/security-model.md Layer 1a for the routing path, built-in defaults, and custom rule format.
-
Create
/workspace/config/thor.json(on the host:docker-volumes/workspace/config/thor.json) fromdocs/examples/thor.json. -
Clone repos into the shared workspace:
docker compose run --rm remote-cli \
git clone https://github.com/your-org/your-repo.gitIf the stack is already running, use docker compose exec remote-cli ... instead.
- Start the stack:
mkdir -p docker-volumes/codex-lb && chmod 777 docker-volumes/codex-lb
docker compose up --build -d
curl http://localhost:8080/global/healthcodex-lb runs as a non-root user and writes a SQLite store to /var/lib/codex-lb; pre-creating the host directory world-writable avoids a root-owned auto-mount.
-
Link a ChatGPT account so opencode has an upstream model:
Visit
http://localhost:8080/dashboard(admin-gated by Vouch +THOR_ADMIN_EMAILS), sign in with Google, and add a ChatGPT account from the codex-lb dashboard. Once linked, opencode picks the model from its UI (the provider whitelist surfacesgpt-5.4,gpt-5.4-mini,gpt-5.5).
Thor is an internal AI teammate for engineering and product work; it is not meant to mirror production infrastructure exactly. Each integration owns its own env vars, app/manifest setup, required permissions, and troubleshooting reasons.
- Slack —
docs/slack.md. Events API intake, signing-secret verification, per-channel repo override, app manifest. - GitHub App —
docs/github.md. Webhook intake, App permissions and event subscriptions, installation IDs, bot commit identity, CI wake gate. - Daytona sandboxes —
docs/daytona.md. On-demand cloud sandboxes for project builds/tests/lints. Custom snapshot publishing. - Outbound HTTP(S) (mitmproxy) —
docs/feat/security-model.mdLayer 1a. Routing path, built-in defaults (Atlassian/Slack/OpenAI), custom credential rules.
Runtime integration paths:
| Integration | Path | Auth | Notes |
|---|---|---|---|
| Git / GitHub CLI | remote-cli /exec/git, /exec/gh |
GitHub App token | Repo-scoped worktree edits |
| Atlassian MCP | remote-cli /exec/mcp |
Auth header | Read + approved writes |
| PostHog MCP | remote-cli /exec/mcp |
API key | Read + approved writes |
| Grafana MCP | remote-cli /exec/mcp |
Service account token | Logs and observability |
| Langfuse MCP | remote-cli /exec/mcp |
API key pair | Read-only LLM observability queries |
| Slack Web API | gateway + remote-cli + OpenCode over mitmproxy |
Bot token | Mentions, progress, approval cards, thread reads/writes |
| LaunchDarkly | remote-cli /exec/ldcli |
Access token | Read-only feature flag inspection |
| Metabase | remote-cli /exec/metabase |
API key | Read-only warehouse access |
Common usage patterns:
- PR merged, errors spike — a scheduled prompt checks telemetry, inspects recent merges through GitHub tools, prepares a fix in a worktree, and requests approval for the final write action.
- Jira issue triage — a webhook or Slack prompt asks Thor to investigate an issue; Thor reads Jira, checks recent commits, and reports likely owners and suspects.
- Daily delivery digest — a cron job asks Thor to summarize stale PRs, blocked issues, or failing tests and post the result to Slack.
Integration-specific env vars live in each integration's doc. Cross-cutting vars:
| Variable | Required | Service | Purpose |
|---|---|---|---|
CRON_SECRET |
Yes | gateway, cron |
Shared secret for cron endpoint auth |
THOR_ADMIN_EMAILS |
Yes | ingress |
Comma-separated authenticated Google emails allowed for OpenCode-backed and /admin/ ingress routes |
THOR_INTERNAL_SECRET |
Yes | remote-cli, gateway |
Secret-gates gateway↔remote-cli internal APIs |
THOR_E2E_TEST_HELPERS |
No | runner |
Enables secret-gated deterministic runner e2e helpers |
RUNNER_BASE_URL |
Yes | remote-cli |
Public base URL for Thor trigger viewer links in PR/Jira content |
INGRESS_PORT |
No | ingress |
Host port for the reverse proxy |
ATLASSIAN_AUTH |
No | remote-cli, mitmproxy |
Global Atlassian MCP auth header and mitmproxy default injection; profile suffixes are MCP-only |
POSTHOG_API_KEY |
No | remote-cli |
Global PostHog MCP auth; profile variants use _<PROFILE_NAME> suffixes |
GRAFANA_URL |
No | remote-cli |
Grafana instance URL; required (with token + org ID) to enable Grafana; profile variants use _<PROFILE_NAME> suffixes |
GRAFANA_SERVICE_ACCOUNT_TOKEN |
No | remote-cli |
Grafana service account token; required (with URL + org ID) to enable Grafana; profile variants use _<PROFILE_NAME> suffixes |
GRAFANA_ORG_ID |
No | remote-cli |
Grafana org ID; required (with URL + token) to enable Grafana; profile variants use _<PROFILE_NAME> suffixes |
LANGFUSE_BASE_URL |
No | remote-cli |
Langfuse MCP base URL; all three LANGFUSE_* vars required to enable Langfuse; profile variants use _<PROFILE_NAME> suffixes |
LANGFUSE_PUBLIC_KEY |
No | remote-cli |
Langfuse MCP public key; required (with secret key + host) to enable Langfuse; profile variants use _<PROFILE_NAME> suffixes |
LANGFUSE_SECRET_KEY |
No | remote-cli |
Langfuse MCP secret key; required (with public key + host) to enable Langfuse; profile variants use _<PROFILE_NAME> suffixes |
METABASE_URL |
No | remote-cli |
Metabase instance URL |
METABASE_API_KEY |
No | remote-cli |
Metabase API key |
METABASE_DATABASE_ID |
No | remote-cli |
Metabase database ID |
METABASE_ALLOWED_SCHEMAS |
No | remote-cli |
Comma-separated schema allowlist |
VOUCH_GOOGLE_CLIENT_ID |
Yes | vouch |
Google OAuth client ID |
VOUCH_GOOGLE_CLIENT_SECRET |
Yes | vouch |
Google OAuth client secret |
VOUCH_JWT_SECRET |
Yes | vouch |
Session JWT signing secret |
VOUCH_ALLOWED_EMAIL_DOMAINS |
No | compose -> vouch |
Rendered into Vouch's VOUCH_DOMAINS; comma-separated email domains, default scoutqa.cc |
VOUCH_CALLBACK_URL |
No | vouch |
OAuth callback URL |
VOUCH_COOKIE_DOMAIN |
No | vouch |
Cookie domain |
Lives at /workspace/config/thor.json inside containers, docker-volumes/workspace/config/thor.json on the host. Hot-reloaded — no restart needed after edits. Use docs/examples/thor.json as a starting point and packages/common/src/proxies.ts as the reference for the built-in upstream catalog.
The file carries operator-maintained registries:
owners.<owner>.github_app_installation_id— GitHub App installation IDs. Seedocs/github.md§2.profiles.<name>.channels[]/profiles.<name>.repos[]— integration credential routing profiles.channels[]also admits gated Slack surfaces;repos[]never admits Slack by itself. Profile shapes, repo fallback, conflict handling, and profile-suffixed environment variables are documented indocs/feat/profile.md. Slack admission details are indocs/slack.md§5.mitmproxy[]/mitmproxy_passthrough[]— outbound credential rules and passthrough hosts. Seedocs/feat/security-model.mdLayer 1a.users[]— human attribution (see below).
Profile edits hot-reload — no service restart needed after thor.json changes. Profile selection is entirely harness-side; there is no agent-facing profile argument.
email must be the Jira account email; Thor may write the name/email into Co-authored-by: commit trailers and use the email to resolve Jira assignees.
{
"users": [
{ "email": "alice@example.com", "name": "Alice", "slack": "UABCDEF1", "github": "alice" },
{ "email": "bob@example.com", "name": "Bob" }
]
}To verify your entry, trigger Thor from Slack and look for attribution_applied with outcome: "applied" and your Slack id; skipped_no_user_record means the configured Slack id did not match the trigger.
The registry is maintained by operators from team Slack and GitHub membership records, with Jira account emails verified manually when needed. Keep source exports out of git if they contain personal data — commit only sanitized reconciliation decisions.
- Tell Thor about durable cross-task context through the memory tiers in
docs/feat/memory.md: global, Slack channel, and person memory are runner-injected; repo context belongs in repo-localAGENTS.md,CLAUDE.md, and docs. - Clone source repos from the
remote-clicontainer so git credentials and filesystem ownership stay consistent. - Repos under
/workspace/reposare mounted read-only into OpenCode. Thor creates edits in/workspace/worktrees. - OpenCode and remote-cli share the same
/tmpvolume so temporary artifacts referenced by absolute path, such asslack-post-message --blocks-file /tmp/..., are readable by the posting service. - Scheduled prompts live in
docker-volumes/workspace/cron/crontab.
Thor contains untrusted input — agent, OpenCode wrappers, external webhooks — through layered controls. In short:
- Vouch SSO + mitmproxy bound the network; remote-cli binds to
127.0.0.1only. - Inbound webhooks are HMAC-verified (Slack signing secret, GitHub
X-Hub-Signature-256); internal gateway↔remote-cli routes are gated withx-thor-internal-secret. - Channel/mention/self-loop gates filter authenticated traffic before it wakes the agent.
remote-cliis the only place tool policy is enforced: MCP allow/approve/hidden tiers, command allowlists, GitHub App installation tokens. OpenCode never holds direct upstream credentials.- Repos mount read-only into OpenCode; edits happen in
/workspace/worktrees. Tool calls are audit-logged under/workspace/worklog.
See docs/feat/security-model.md for the full layered breakdown.
pnpm test
REMOTE_CLI_GIT_REPO_URL=https://github.com/owner/repo \
REMOTE_CLI_GITHUB_REPO=owner/repo \
pnpm test:e2e
pnpm test:create-jira-approval-e2e # live Slack/OpenCode approval-card e2e for Atlassian approval-required tools
pnpm test:opencode-e2e # separate explicit OpenCode/LLM smoke path
pnpm typecheck