Tự host OpenTag để nhận @mention cho coding agent
Chạy OpenTag v0.9.0 trên VPS để chuyển mention từ Slack và GitHub thành job cho coding agent, với TLS, kiểm tra webhook signature và token scope an toàn.
OpenTag thực hiện gì khi bạn mention một agent
OpenTag biến một @mention trong thread Slack hoặc issue GitHub thành một lần chạy coding agent trên máy do bạn sở hữu. Ai đó comment @opentag investigate this trong một issue. Listener nhận event từ platform, kiểm tra signature, đối chiếu mention với project đã bind, khởi chạy coding agent trên một local checkout, rồi đăng kết quả lại vào cùng thread.
Project này được cấp phép theo MIT và có mã nguồn tại amplifthq/opentag. Tính đến tháng 8 năm 2026, release được tag mới nhất là v0.9.0, phát hành ngày 28 tháng 7 năm 2026, và được cung cấp dưới dạng npm package. Không có container image chính thức, vì vậy thứ bạn cần pin là phiên bản npm. Mọi lệnh bên dưới đều pin phiên bản này.
Đây là project phù hợp với VPS thay vì laptop vì phần GitHub. GitHub gửi repository event bằng cách thực hiện HTTP request đến một URL mà bạn đăng ký một lần. Vì vậy, URL đó phải phản hồi tại cùng một địa chỉ vào ngày mai.
Bốn thành phần chính
Listener nhận các event từ platform; mỗi platform có một listener riêng. GitHub listener là một HTTP endpoint trên port 3050 tại đường dẫn /github/webhooks. Slack Events API listener chạy trên port 3040 tại /slack/events. Slack cũng hỗ trợ Socket Mode. Ở chế độ này, app mở một WebSocket outbound nên không cần inbound port.
Dispatcher là thành phần điều phối. Theo mặc định, nó lắng nghe trên port 3030, lưu trạng thái run trong file database cục bộ được chỉ định bởi OPENTAG_DATABASE_PATH và ghi audit trail cho mọi run. Không được để bất kỳ hệ thống nào bên ngoài máy truy cập port này.
Runner là daemon cục bộ. Nó thăm dò công việc, nhận một run, giữ lease cho run đó và mặc định gửi heartbeat mỗi 15 giây trong khi run còn hoạt động. Nó từ chối mọi run đã nhận nếu project target bị thiếu hoặc nằm ngoài allowlist trong config riêng của nó. Đây là bước kiểm tra ngăn một GitHub event trỏ agent đến repository mà bạn chưa bind.
Executor chính là coding agent. OpenTag khởi chạy executor qua ACP (agent client protocol), một giao thức JSON-RPC trao đổi qua standard input và standard output. Vì vậy, agent chạy dưới dạng child process trong working directory do OpenTag cung cấp. Các tên tích hợp gồm echo, codex, claude-code, cursor, opencode, hermes và openclaw. Hãy bắt đầu với echo, executor được cung cấp trong config mẫu, vì nó xác nhận toàn bộ luồng hoạt động trước khi model tác động đến code của bạn.
Thứ tự luôn cố định: platform event, kiểm tra chữ ký, tạo run record, nhận run, agent, trả lời trong thread.
Vì sao laptop và tunnel vẫn chưa đủ
Hướng dẫn thiết lập GitHub yêu cầu bạn chạy ngrok http 3050 rồi dán hostname của tunnel vào webhook của repository. Cách này chỉ hoạt động trong 10 phút đầu. Host của tunnel miễn phí thay đổi mỗi khi process khởi động lại và không còn tồn tại khi laptop chuyển sang chế độ ngủ. GitHub vẫn giữ payload URL cũ và tiếp tục gửi request đến đó, nên tab Recent Deliveries trong phần cài đặt webhook sẽ đầy lỗi trong khi thread vẫn im lặng. Không ai nhận ra trong một tuần, vì một webhook không làm gì trông chẳng khác nào một bot mà không ai nhắc đến.
VPS khắc phục được 2 nguyên nhân này. Tên DNS không thay đổi, nên payload URL bạn dán một lần vẫn luôn đúng. Máy không chuyển sang chế độ ngủ, nên một comment lúc 02:00 vẫn nhận được câu trả lời. Hãy thiết lập máy đúng cách trước: 10 phút đầu tiên trên một VPS mới bao gồm user đăng nhập và firewall mà hướng dẫn này giả định bạn đã có.
Slack là ngoại lệ. Ở Socket Mode, Slack kết nối outbound và không cần public URL, nên deployment chỉ dùng Slack có thể vẫn đóng với Internet. GitHub không có cơ chế tương đương. Webhook của repository là HTTP inbound, nên cần một endpoint public, cần TLS (transport layer security) và cần kiểm tra chữ ký.
Tự host OpenTag trên Ubuntu từ bản release cố định
OpenTag v0.9.0 yêu cầu Node.js 22 trở lên. Ubuntu 24.04 có Node 18 trong repository riêng, vì vậy hãy cài từ NodeSource.
curl -fsSL https://deb.nodesource.com/setup_22.x -o nodesource_setup.sh
sudo -E bash nodesource_setup.sh
sudo apt install -y nodejs
node -vnode -v phải in ra v22 hoặc cao hơn. Trên Node 20, quá trình cài đặt in cảnh báo EBADENGINE và CLI có thể fail ngay khi khởi động.
Tạo một account riêng cho service. Agent chạy với quyền của user này, vì vậy không dùng account đăng nhập của bạn và cũng không dùng root. User có đặc quyền tối thiểu trên VPS giải thích vì sao nên tách riêng dù phải thêm một bước.
sudo adduser --disabled-password --gecos "" opentag
sudo loginctl enable-linger opentag
sudo npm install -g @opentag/cli@0.9.0
command -v opentagcommand -v opentag phải in ra một path như /usr/bin/opentag. Cấu hình linger quan trọng trên Linux: OpenTag cài background service thông qua systemd, và user service không có linger sẽ dừng ngay khi phiên SSH của bạn đóng.
Chạy setup bằng user đó.
sudo -iu opentag opentag setupSetup hỏi 6 thông tin: ngôn ngữ của CLI, địa chỉ listening nội bộ, coding agent, project local cần xử lý, thông tin xác thực của platform cần lưu và cách chạy. Giữ địa chỉ listening ở 127.0.0.1, vì nginx thực hiện TLS termination rồi chuyển tiếp đến địa chỉ này. Nhờ đó các listener không cần reachable từ bên ngoài. Với GitHub, setup cũng hỏi repository theo dạng owner/repo, có được phép mở pull request hay không, port webhook (mặc định là 3050) và token. Cuối cùng, chọn chế độ background service. Nếu đã có config và muốn cài service mà không cần prompt, opentag setup --service sẽ thực hiện việc đó.
Config được ghi vào /home/opentag/.config/opentag/config.json, còn trạng thái runtime được ghi vào /home/opentag/.local/state/opentag. Sau khi setup ghi file, nên tự kiểm tra các key này.
{
"runnerId": "runner_local",
"dispatcherUrl": "http://localhost:3030",
"runnerToken": "...",
"approvalMode": "ask",
"repositories": []
}Ưu tiên runnerToken, bearer token dành riêng cho runner, thay vì pairingToken dùng chung kiểu cũ. Config file lưu credential dưới dạng plain text, trừ khi bạn thay bằng secret reference. Secret reference đọc giá trị từ environment hoặc từ một file trên disk khi khởi động. Dù dùng cách nào, file này vẫn là dữ liệu nhạy cảm nhất trên máy: đặt mode 600, thuộc sở hữu của opentag và tuyệt đối không đặt trong git repository. Phần giải thích rộng hơn có trong giữ secret ngoài AI agent.
Kiểm tra việc cài đặt trước khi expose bất kỳ thứ gì.
sudo -iu opentag opentag doctor
sudo -iu opentag opentag statusopentag doctor kiểm tra dispatcher, binding, checkout và executor. opentag status in config và trạng thái runtime; sau khi đã có run, có thể giới hạn kiểm tra vào một run duy nhất. Hãy xử lý mọi vấn đề mà doctor báo cáo trước khi trỏ platform vào máy này.
Đặt TLS phía trước và chỉ mở hai đường dẫn
nginx xử lý TLS và chỉ chuyển tiếp chính xác hai đường dẫn. Mọi yêu cầu khác đều trả về 404, nên scanner chỉ phát hiện được host cũng không biết phía sau đang chạy gì.
Tạo một server block thuần cho port 80 tại /etc/nginx/sites-available/opentag với hai location bên dưới, sau đó để Certbot thêm phần TLS.
sudo apt install -y nginx certbot python3-certbot-nginx
sudo ln -s /etc/nginx/sites-available/opentag /etc/nginx/sites-enabled/opentag
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d opentag.example.comnginx -t in ra syntax is ok và test is successful. Đây là bước duy nhất ngăn một lỗi đánh máy gây ra lần reload làm site ngừng hoạt động. Certbot trên Ubuntu 24.04 với nginx giải thích cách gia hạn và các nguyên nhân khiến ACME (automatic certificate management environment) challenge thất bại. Block hoàn chỉnh như sau.
server {
listen 443 ssl;
server_name opentag.example.com;
ssl_certificate /etc/letsencrypt/live/opentag.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/opentag.example.com/privkey.pem;
client_max_body_size 2m;
location = /github/webhooks {
proxy_pass http://127.0.0.1:3050;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
location = /slack/events {
proxy_pass http://127.0.0.1:3040;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}
location / {
return 404;
}
}= trong location = /github/webhooks là exact match, còn proxy_pass không có gì phía sau port sẽ chuyển tiếp URI gốc mà không thay đổi. Bỏ = thì mọi path bên dưới /github/webhooks/ cũng được chuyển tiếp, làm tăng bề mặt hơn mức listener cần.
Firewall vẫn được giới hạn.
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw statusCác port 3030, 3040 và 3050 không bao giờ được mở. Xác nhận chúng bind vào loopback thay vì mọi interface.
sudo ss -tlnpMọi dòng OpenTag phải có dạng 127.0.0.1:3030 hoặc tương tự. Dòng có dạng 0.0.0.0:3050 nghĩa là listener đang cung cấp dịch vụ cho toàn bộ Internet và chỉ ufw đang ngăn truy cập. Chỉ cần một lỗi cấu hình firewall là agent trigger sẽ bị mở. Kiến thức cơ bản về firewall ufw giải thích chính sách deny mặc định đó thực sự làm gì.
Hai phép kiểm tra xác nhận front door. curl -I https://opentag.example.com/ trả về 404 từ nginx, cho thấy certificate hợp lệ và catch-all đã bị đóng. Yêu cầu tới /slack/events hoặc /github/webhooks không có signature không bao giờ được trả về 200.
Xác minh mọi chữ ký vì URL là public
Bất kỳ ai cũng có thể tìm thấy URL của payload. URL này có thể nằm trong phần cài đặt repository, lịch sử trình duyệt hoặc ảnh chụp màn hình được dán vào ticket. Chữ ký là yếu tố duy nhất phân biệt một delivery hợp lệ từ GitHub với request do ai đó tự nhập.
GitHub ký từng delivery bằng webhook secret và gửi kết quả trong header x-hub-signature-256. OpenTag xác minh header đó bằng platforms.github.webhookSecret. Ghi chú hardening của project nêu rõ quy tắc này: không chấp nhận source event không có chữ ký trên /github/webhooks. Slack ký từng request bằng SLACK_SIGNING_SECRET và kèm timestamp, nên body bị capture không thể bị replay sau nhiều giờ.
Bỏ qua bước này tạo ra rủi ro nghiêm trọng. Endpoint không xác minh sẽ chấp nhận payload issue_comment được tự viết, chứa @opentag. Sau đó OpenTag chạy coding agent bằng token của bạn, trong checkout của bạn, theo chỉ dẫn của người lạ. Phản hồi sẽ được gửi đến thread mà payload giả mạo chỉ định.
OpenTag bổ sung thêm 2 lớp bảo vệ. Source delivery được theo dõi bằng delivery ID, nên redeliver cùng một event không khởi chạy lần chạy thứ 2. Runner call chấp nhận idempotency key, nên replay một request sẽ trả về success mà không thêm audit event khác.
Có thể cấu hình rate limit và nên bật chúng. OPENTAG_RATE_LIMIT_WINDOW_MS và OPENTAG_RATE_LIMIT_MAX_REQUESTS giới hạn tốc độ request, OPENTAG_MAX_REQUEST_BODY_BYTES giới hạn kích thước body, còn payload quá lớn sẽ bị từ chối với 413 request_body_too_large. OPENTAG_RATE_LIMIT_DISABLED=true dành cho môi trường phát triển local và không được dùng trên public box. Một quy tắc khác trong cùng các ghi chú: relay URL public phải dùng HTTPS; CLI chỉ cho phép HTTP thuần trên localhost.
Bot thực sự cần những token scope nào?
Trên GitHub, OpenTag dùng personal access token có scope chi tiết thay vì GitHub App. Tài liệu cho biết hướng GitHub App đã được lên kế hoạch và hiện chưa phải cách thiết lập CLI mặc định. Điều này dẫn đến một điểm thường bị bỏ qua: bot bình luận dưới danh nghĩa người đã tạo token. Hãy tạo token dưới một account mà bạn chấp nhận nhìn thấy tên mình trong mọi câu trả lời triage.
Giới hạn scope đúng như hướng dẫn thiết lập. Chọn Only select repositories rồi chọn một repository. Cấp Issues: Read and write và Pull requests: Read and write. Như vậy là đủ để đọc một mention và trả lời trong thread.
Hãy chú ý những quyền không có: quyền ghi vào code. OpenTag không push branch trừ khi preparePullRequestBranch được đặt thành true. Ngoài ra còn có githubApplyToken riêng để token ghi code không phải là token ghi comment. Giữ hai token tách biệt. Tắt token ghi code cho đến khi quy trình chỉ đọc và bình luận đã chạy ổn định trong vài tuần.
Không nên dùng token có Contents: Read and write trên All repositories. Khi đó, bất kỳ ai có quyền bình luận trong một repository thuộc phạm vi này đều có thể điều khiển agent có quyền commit. Audit trail sẽ ghi nhận thao tác đó là do chủ sở hữu token thực hiện. Chỉ mở rộng scope thêm từng repository một, sau khi agent đã chứng minh được độ tin cậy.
Trên Slack, bot cần các scope app_mentions:read, chat:write, reactions:write và channels:history. Private channel cũng cần groups:history và phải subscribe vào event message.groups. Socket Mode cần một app-level token có connections:write, tức token bắt đầu bằng xapp-. channels:history đọc message history trong các public channel mà bot đã được thêm vào. Vì vậy, hãy thêm bot vào đúng những channel cần dùng thay vì thêm vào mọi channel.
Đi qua một issue từ đầu đến cuối
Webhook là bước đầu tiên. Trong repository, mở Settings, rồi Webhooks, rồi Add webhook. Payload URL là https://opentag.example.com/github/webhooks, content type là application/json, còn secret là giá trị do setup tạo. Chỉ đăng ký Issue comments và Pull request review comments, không chọn mục nào khác.
GitHub gửi một ping delivery ngay sau khi bạn lưu. Mở Recent Deliveries và kiểm tra request có đến được server hay không. Mã 502 ở đây có nghĩa là nginx không thể kết nối đến listener. Đây là vấn đề cục bộ, không phải vấn đề của GitHub.
Bây giờ hãy sử dụng nó. Mở một issue mô tả lỗi và thêm comment:
@opentag triage this. Reproduce the report against the current main branch, then reply with the file and function most likely responsible, plus the test you would write first.Luồng xử lý đúng theo thứ tự như sau. Recent Deliveries ghi nhận delivery issue_comment với response 2xx. Dispatcher ghi nhận một run. Runner nhận run đó và bắt đầu gửi heartbeat. Executor mở checkout và xử lý công việc. Câu trả lời xuất hiện dưới dạng comment trong cùng issue thread. sudo -iu opentag opentag status hiển thị run khi run đang thực hiện, để bạn theo dõi thay vì phỏng đoán.
Đặt approvalMode thành ask trước lần chạy thực tế đầu tiên. Ở chế độ ask, run sẽ tạm dừng và chờ một người xác nhận trước khi thực hiện bất kỳ thay đổi trạng thái nào. Các chế độ auto và autonomous cũng có sẵn. Bạn có thể dùng chúng sau này, trên một repository mà bạn đã đọc transcript trong một tháng.
Ở phía Slack, run tương tự bắt đầu bằng /bind owner/repo trong channel, sau đó là một mention. Bot cũng trả lời /help, /status, /doctor, /stop và /unbind confirm. Giới hạn người được phép thay đổi binding bằng OPENTAG_SLACK_BINDING_ADMIN_USER_IDS, là danh sách Slack user ID phân tách bằng dấu phẩy, vì binding là ánh xạ từ một channel public đến một checkout trên server của bạn.
Triage là route đầu tiên phù hợp vì nó chỉ đọc dữ liệu, không ghi dữ liệu, và câu trả lời dễ đánh giá. Review là bước tiếp theo, trong đó agent comment trên diff thay vì issue: agent review pull request tự host là cùng kiến trúc này nhưng hướng đến pull request. Nếu muốn agent truy cập các hệ thống của riêng bạn trong khi làm việc, hãy dùng MCP server trên VPS.
Điều gì xảy ra khi agent trả lời sai trước mặt mọi người?
Agent sẽ trả lời sai. Vấn đề là sai như vậy gây ra hậu quả gì.
Một câu trả lời sai trên issue công khai sẽ trở thành comment dưới tên mà team của bạn nhận ra. GitHub gửi email cho tất cả người đã subscribe ngay khi comment được đăng. Xóa comment không thu hồi được email đó. Slack notification cũng tương tự. Hãy thiết kế theo hướng câu trả lời có thể sai công khai, thay vì giả định rằng nó sẽ đúng khi chạy riêng tư.
Bốn lựa chọn sau sẽ giới hạn mức độ thiệt hại. Chúng quan trọng hơn bất kỳ prompt nào bạn viết.
- Chạy ở chế độ
askđể agent đề xuất, một người phê duyệt, và một plan sai chỉ tốn một lần click. - Giữ
preparePullRequestBranchở giá trị mặc định là false để hậu quả nghiêm trọng nhất của một lần chạy sai là một comment sai, không phải một branch sai. - Ban đầu chỉ bind một repository và một channel. Runner từ chối mọi lần chạy có project target nằm ngoài allowlist cục bộ. Vì vậy, một repository chưa được bind không thể tự kéo agent vào.
- Giữ token dùng để comment tách biệt với mọi apply token. Khi đó, thu hồi quyền ghi sẽ không làm hệ thống triage ngừng hoạt động.
Slack có command /stop để dừng một lần chạy đang đi sai hướng. Mỗi lần chạy cũng tạo một audit record chứa mention đã khởi động lần chạy và các thao tác agent đã thực hiện. Sau đó, bạn đọc record này để xác định agent đã sai ở đâu.
Phần giao tiếp với người dùng quan trọng không kém cấu hình. Đưa bot vào một channel nơi mọi người biết đang tương tác với máy và hiểu rằng bot có thể trả lời sai. Một câu trả lời sai nhưng có vẻ chắc chắn trong channel có 40 người, khi mọi người tưởng rằng đã có người kiểm tra, sẽ gây thiệt hại lớn hơn thời gian tiết kiệm được nhờ triage. Ghi rõ trong phần mô tả channel ai phụ trách bot và ai kiểm tra output của bot.
Sao lưu, nâng cấp và pin phiên bản
Hai đường dẫn chứa toàn bộ dữ liệu: /home/opentag/.config/opentag/config.json và /home/opentag/.local/state/opentag. Đường dẫn thứ nhất chứa credential của bạn. Đường dẫn thứ hai chứa lịch sử chạy và file database. Sao lưu cả hai với mode 600 và lưu ngoài máy chủ. Nếu mất chúng, bạn phải tạo lại token và binding, không phải dựng lại máy chủ.
Nâng cấp chỉ gồm tăng version và restart.
sudo npm install -g @opentag/cli@0.9.0
sudo -iu opentag opentag service stop
sudo -iu opentag opentag service start
sudo -iu opentag opentag doctorPin version thay vì theo dõi @latest. Phần mềm này chạy coding agent trên repository của bạn bằng token đang hoạt động. Vì vậy, một release được publish qua đêm là một thay đổi chưa được review đối với repository đó. Security policy không backport bản sửa lỗi nào. Các bản sửa lỗi chỉ có trong release mới nhất. Vì vậy, pin version nghĩa là đọc changelog và chủ động nâng cấp. Điều này không có nghĩa là giữ vĩnh viễn v0.9.0. Lịch sử đến tháng 7 năm 2026 cho thấy mỗi tháng có vài release. Đây là lý do phù hợp để đọc release notes trước mỗi lần tăng version.
FAQ
Tôi có cần VPS để chạy OpenTag không, hay laptop là đủ?
Laptop là đủ nếu chỉ dùng Slack, vì Socket Mode mở WebSocket outbound và không cần cổng inbound. GitHub thì khác. Repository webhook gửi request qua HTTP inbound đến URL bạn đăng ký một lần, nên địa chỉ này phải giữ nguyên và phải phản hồi cả khi bạn đang ngủ. Tunnel host từ tài khoản miễn phí thay đổi sau mỗi lần restart, còn GitHub vẫn tiếp tục gửi đến địa chỉ cũ. Điều này xuất hiện dưới dạng các lần gửi thất bại trong tab Recent Deliveries của repository và thread không có phản hồi. VPS có DNS name cố định và certificate sẽ giải quyết cả hai vấn đề.
OpenTag cần những quyền GitHub nào?
Một fine-grained personal access token chỉ giới hạn ở Only select repositories, với Issues: Read and write và Pull requests: Read and write. Các quyền này đủ để đọc mention và trả lời trong thread. Không cần quyền ghi vào code, trừ khi bạn đặt preparePullRequestBranch thành true để OpenTag push branch. Ngoài ra có githubApplyToken riêng để giữ token ghi code tách khỏi token dùng để comment. Tránh dùng token cho toàn bộ repository với quyền contents write, vì bất kỳ ai có thể comment trên một trong các repository đó đều có thể điều khiển agent có khả năng commit.
Làm cách nào để dừng một run đang gặp lỗi?
Slack có command /stop dành đúng cho việc này. Trên server, opentag status hiển thị những gì đang chạy, còn opentag service stop dừng daemon và kết thúc toàn bộ pipeline thay vì chỉ một run. Để không phải dùng hai lệnh này, đặt approvalMode thành ask để run tạm dừng chờ người duyệt trước khi thay đổi bất kỳ thứ gì. Giữ preparePullRequestBranch ở false để một run lỗi tạo comment thay vì branch.
Vì sao webhook trả về 502 nhưng thread vẫn không có phản hồi?
502 đến từ nginx, không phải OpenTag. Lỗi này có nghĩa là proxy không thể kết nối đến listener. /var/log/nginx/error.log sẽ hiển thị connect() failed (111: Connection refused) while connecting to upstream. Listener có thể đã dừng hoặc đang chạy trên port khác với port được ghi trong dòng proxy_pass. Chạy sudo ss -tlnp và xác nhận có tiến trình đang listening trên 127.0.0.1:3050 cho GitHub và 127.0.0.1:3040 cho Slack. Sau đó chạy opentag doctor để kiểm tra các binding và executor.