Tự host OpenTag để nhận mention @agent
Chạy OpenTag trên VPS để mention từ Slack và GitHub gọi coding agent: cấu hình TLS, kiểm tra webhook signature, token scopes và pin npm v0.9.0.
OpenTag làm 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 bạn sở hữu. Ai đó comment @opentag investigate this trên một issue. Listener nhận event từ platform, kiểm tra chữ ký, đối chiếu mention với project đã bind, khởi chạy coding agent trên một bản checkout cục bộ, rồi đăng kết quả trở lại cùng thread.
Project này được cấp phép theo MIT và nằm tại amplifthq/opentag. Tính đến tháng 8 năm 2026, release mới nhất được tag là v0.9.0, phát hành ngày 28 tháng 7 năm 2026, và được đóng gói dưới dạng npm package. Không có container image chính thức, nên thứ bạn cần pin là phiên bản npm. Mọi command bên dưới đều pin phiên bản đó.
Đâ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 URL bạn đăng ký một lần, nên 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 event từ nền tảng, và mỗi nền tảng có một listener riêng. Listener của GitHub là một HTTP endpoint trên port 3050 tại đường dẫn /github/webhooks. Listener của Slack Events API chạy trên port 3040 tại /slack/events. Slack cũng có thể chạy ở Socket Mode. Khi đó app mở một WebSocket outbound và không cần mở inbound port.
Dispatcher là thành phần điều phối. Theo mặc định, nó listen trên port 3030, lưu trạng thái run trong file database cục bộ được đặt bởi OPENTAG_DATABASE_PATH và ghi audit trail cho mọi run. Không được để bất kỳ kết nối nào từ bên ngoài truy cập port này.
Runner là daemon cục bộ. Nó poll để tìm công việc, claim 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 đã được claim nếu project target bị thiếu hoặc nằm ngoài allowlist trong config của chính nó. Đây là bước kiểm tra ngăn một event từ GitHub trỏ agent của bạn đế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à output. Vì vậy agent chạy dưới dạng child process trong working directory do OpenTag cấp. Các tên built-in gồm echo, codex, claude-code, cursor, opencode, hermes và openclaw. Hãy bắt đầu với echo, executor có sẵn 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 vào code của bạn.
Thứ tự luôn cố định: event từ nền tảng, kiểm tra chữ ký, tạo run record, claim, agent, trả lời trong thread.
Vì sao laptop và tunnel là 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 host của tunnel vào webhook của repository. Cách này chỉ hoạt động trong mười phút đầu. Host 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 thử gửi đến đó, nên tab Recent Deliveries trong phần cài đặt webhook liên tục hiển thị lỗi còn thread thì 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 giống hệt một bot mà không ai nhắc đến.
VPS khắc phục được hai nguyên nhân gây lỗi 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 comment lúc 02:00 vẫn nhận được phản hồi. Trước hết, hãy thiết lập máy đúng cách: mười phút đầu tiên trên VPS mới trình bày user đăng nhập và firewall mà hướng dẫn này giả định.
Slack là ngoại lệ. Ở Socket Mode, Slack kết nối ra ngoài và không cần URL public, nên deployment chỉ dùng Slack có thể không public endpoint. GitHub không có cơ chế tương đương. Webhook của repository là HTTP inbound, tức là cần một endpoint public, đồng nghĩa với việc cần TLS (bảo mật lớp truyền tải) và kiểm tra chữ ký.
Tự host OpenTag trên Ubuntu từ một bản phát hành cố định
OpenTag v0.9.0 yêu cầu Node.js 22 trở lên. Ubuntu 24.04 cung cấp Node 18 trong repository riêng, vì vậy hãy cài đặt 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à không dùng root. User áp dụng nguyên tắc quyền tối thiểu trên VPS giải thích vì sao nên thực hiện thêm bước này.
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 đường dẫn như /usr/bin/opentag. Thiết lập 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 cục bộ, coding agent, project cục bộ cần làm việc, 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 và chuyển tiếp đến địa chỉ này, nên các listener không cần có thể truy cập 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 trả lời prompt, opentag setup --service sẽ thực hiện việc đó.
Config được lưu tại /home/opentag/.config/opentag/config.json và runtime state tại /home/opentag/.local/state/opentag. Sau khi setup ghi file, nên kiểm tra thủ công các key này.
{
"runnerId": "runner_local",
"dispatcherUrl": "http://localhost:3030",
"runnerToken": "...",
"approvalMode": "ask",
"repositories": []
}Ưu tiên runnerToken, bearer token giới hạn theo runner, thay cho pairingToken dùng chung kiểu cũ. File config lưu thông tin xác thực 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 nằm trong git repository. Phần giải thích rộng hơn có trong giữ secret ngoài AI agent.
Kiểm tra quá trình 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, các binding, checkout và executor. opentag status in config và runtime state, đồng thời có thể giới hạn phạm vi vào một run khi đã có run. Sửa mọi vấn đề mà doctor báo cáo trước khi trỏ một 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à 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 sẽ không biết phía sau đang chạy gì.
Tạo một server block cho port 80 tại /etc/nginx/sites-available/opentag với hai location bên dưới, rồi để 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 syntax is ok và test is successful. Đây là lớp bảo vệ duy nhất giữa một lỗi đánh máy và lần reload làm site ngừng hoạt động. Certbot trên Ubuntu 24.04 với nginx trình bày cách gia hạn và các nguyên nhân khiến ACME (môi trường quản lý chứng chỉ tự động) 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ì sau port sẽ chuyển tiếp URI gốc mà không thay đổi. Bỏ = thì mọi đường dẫn 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 statusKhông mở các port 3030, 3040 và 3050. Xác nhận chúng bind vào loopback thay vì bind vào 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 chặn nó. Chỉ cần một lỗi trong firewall là agent trigger có thể bị mở ra. Kiến thức cơ bản về firewall ufw giải thích chính xác default deny đang làm gì.
Hai phép kiểm tra sẽ xác nhận front door. curl -I https://opentag.example.com/ trả về 404 từ nginx, cho thấy chứng chỉ hợp lệ và catch-all đã bị đóng. Yêu cầu đến /slack/events hoặc /github/webhooks không có signature tuyệt đối không được trả về 200.
Xác minh mọi chữ ký vì URL là công khai
Bất kỳ ai cũng có thể tìm thấy URL nhận payload. URL này 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 thực từ GitHub với một 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 đó với platforms.github.webhookSecret. Ghi chú hardening của dự án 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à thê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. Một 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 được gửi đến bất kỳ thread nào 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 động lần chạy thứ 2. Runner call chấp nhận idempotency key, nên replay một call sẽ trả về success mà không thêm audit event khác.
Có thể cấu hình rate limit và phải 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 development cục bộ và không được dùng trên máy chủ công khai. Một quy tắc khác trong cùng các ghi chú: relay URL công khai phải dùng HTTPS, còn CLI chỉ cho phép HTTP thuần với localhost.
Bot thực sự cần những token scope nào?
Trên GitHub, OpenTag sử dụng personal access token dạng fine-grained thay vì GitHub App. Tài liệu cho biết phương án App đang đượ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 nhiều người bỏ qua: bot sẽ bình luận dưới danh tính của người đã tạo token. Hãy tạo token trong một account mà bạn chấp nhận việc tên account đó xuất hiện trong mọi câu trả lời phân loại sự cố.
Giới hạn scope đúng như hướng dẫn thiết lập. Chọn Only select repositories và chọn một repository. Cấp quyền Issues: Read and write và Pull requests: Read and write. Như vậy là đủ để đọc một mention và trả lời trong thread.
Lưu ý quyền đang bị bỏ qua: quyền ghi vào code. OpenTag không push branch trừ khi preparePullRequestBranch được đặt thành true. Có một githubApplyToken riêng để token ghi code không trùng với token ghi bình luận. Hãy tách riêng hai token này và giữ token ghi ở trạng thái chưa bật cho đến khi luồng đọc và bình luận đã chạy ổn định trong vài tuần.
Cấu hình cần tránh là một token có quyền Contents: Read and write trên All repositories. Khi đó, bất kỳ ai có thể bình luận trên một trong các repository đó đều có thể điều khiển một agent có quyền commit, còn audit trail sẽ ghi rằng chủ sở hữu token đã thực hiện thao tác đó. Chỉ mở rộng scope từng repository một, sau khi agent đã chứng minh được độ tin cậy.
Trên Slack, các bot scope là app_mentions:read, chat:write, reactions:write và channels:history. Private channel cũng cần groups:history cùng với subscription cho event message.groups. Socket Mode cần một app-level token có connections:write, token bắt đầu bằng xapp-. channels:history cho phép đọc lịch sử tin nhắn 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.
Xử lý trọn vẹn một vấn đề
Webhook được cấu hình trước. 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ị mà quá trình setup đã tạo. Chỉ đăng ký Issue comments và Pull request review comments, không đăng ký sự kiện 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. Nếu nhận 502, đó là nginx báo không thể kết nối đến listener. Đây là vấn đề cục bộ, không phải vấn đề từ GitHub.
Bây giờ hãy dùng thử. Mở một issue mô tả lỗi và 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 đúng phải diễn ra 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 thread của issue. sudo -iu opentag opentag status hiển thị run trong lúc đang chạy, để bạn theo dõi thay vì phải đoán.
Đặt approvalMode thành ask trước lần chạy thật đầu tiên. Ở chế độ ask, run sẽ tạm dừng và chờ 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 tồn tại. 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, cùng run đó 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, một danh sách Slack user ID được phân tách bằng dấu phẩy, vì binding là ánh xạ từ một public channel đế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ì trên issue: một pull request review agent tự host dùng cùng kiến trúc này nhưng trỏ vào 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, đó là nhiệm vụ của MCP server trên VPS. Web search là capability khác mà triage thường yêu cầu, còn kết nối agent với SearXNG instance của riêng bạn giúp các lượt tra cứu chạy trên phần cứng do bạn quản lý, đổi lại có thêm một kênh để text từ người lạ đi vào agent.
Điều gì xảy ra khi agent trả lời sai trước mặt mọi người?
Agent sẽ có lúc 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 một comment dưới tên mà team của bạn nhận ra, và GitHub sẽ gửi email cho tất cả người đăng ký ngay khi comment được đăng. Xóa comment không thu hồi được email đó. Slack notification cũng vậy. Hãy thiết kế cho tình huống câu trả lời sai xuất hiện công khai, thay vì giả định nó sẽ đúng trong private.
Bốn lựa chọn sau giới hạn mức độ thiệt hại và quan trọng hơn mọi prompt bạn viết.
- Chạy ở chế độ
askđể agent đề xuất, một người phê duyệt, và một plan sai chỉ gây ra một lần click. - Giữ
preparePullRequestBranchở giá trị mặc định là false để kết quả tệ nhất của một lần chạy lỗi là một comment sai, thay vì một branch sai. - Ban đầu chỉ liên kết 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ộ, nên một repository chưa được liên kết không thể tự kéo agent vào đó.
- Giữ token dùng để comment tách biệt với mọi token apply, để việc thu hồi quyền ghi không làm chức năng triage dừng theo.
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 chạy lần chạy đó và các thao tác agent đã thực hiện. Bạn sẽ đọc record này sau đó để xác định lỗi xảy ra ở đâu.
Khía cạnh giao tiếp cũng quan trọng không kém cấu hình. Đặt bot trong một channel nơi mọi người biết đó là bot và hiểu rằng nó có thể trả lời sai. Một câu trả lời sai nhưng đầy tự tin trong channel có 40 người, nơi mọi người nghĩ đã có người kiểm tra, sẽ gây thiệt hại lớn hơn lợi ích triage mang lại. Ghi rõ trong phần mô tả channel ai phụ trách bot và ai kiểm tra output của bot.
Bản sao lưu, nâng cấp và ghim 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 thông tin xác thực của bạn, đường dẫn thứ hai chứa lịch sử chạy và tệp cơ sở dữ liệu. Hãy sao lưu cả hai với mode 600 và lưu bên ngoài máy chủ. Nếu mất các tệp này, bạn phải tạo lại token và binding, chứ không phải dựng lại máy chủ.
Nâng cấp gồm đổi phiên bản 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 doctorHãy ghim phiên bản thay vì theo dõi @latest. Phần mềm này chạy một coding agent trên repository của bạn với token đang hoạt động, nên một release được phát hành qua đêm sẽ trở thành một thay đổi chưa được review đối với repository đó. Chính sách bảo mật không backport các bản sửa lỗi, và bản sửa lỗi chỉ được đưa vào release mới nhất. Vì vậy, ghim phiên bản giúp bạn đọc changelog và chủ động nâng cấp. Điều đó không có nghĩa là phải 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ó nhiều release, vì vậy nên đọc release notes trước mỗi lần tăng phiên bản.
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ỉ đó 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 mục gửi thất bại trong tab Recent Deliveries của repository và không có phản hồi trong thread. VPS có tên DNS cố định và certificate sẽ loại bỏ cả hai vấn đề này.
OpenTag cần những quyền GitHub nào?
Một personal access token fine-grained 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ó một githubApplyToken riêng để tách token ghi code khỏi token dùng để comment. Tránh dùng token áp dụng cho tất cả repository và có 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 một agent có quyền commit.
Làm cách nào để dừng một run đang gặp lỗi?
Slack có command /stop dành riê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ỉ dừng 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 xác nhận trước khi thay đổi bất kỳ điều gì. Đồng thời giữ preparePullRequestBranch ở false để run lỗi tạo comment thay vì tạo branch.
Tại sao webhook trả về 502 trong khi thread vẫn không có phản hồi?
502 đến từ nginx, không phải OpenTag. Lỗi này 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 cổng khác với cổng được nêu trong dòng proxy_pass. Chạy sudo ss -tlnp và xác nhận có tiến trình đang listen 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.