intent-gate-service
August 11, 2026 · View on GitHub
English | 简体中文
The DingTalk-interaction sister project of intent-gate (a standalone MCP service). It carries the two heavy assets the main plugin deliberately shed: the DingTalk group consensus channel and the blocking decision gates. The main plugin intent-gate stays lightweight (intent alignment + requirement analysis, zero credentials, zero dependencies); all heavy interaction that blocks waiting for a human reply is concentrated here.
intent-gate (light, required) intent-gate-service (heavy, optional)
───────────────────────────── ──────────────────────────────────
requirement-analysis playbook / lint blocking decision gate ask_human
/ anchors group channel group_dispatch / nudging
single-channel intent alignment DingTalk outbound + inbound
(chat-dialog fallback) (http/stream)
collect / resolve / readiness self-check ⇆
shared file contract: .harness/requests/{requirement}/_review/
The two services are joined by a file contract: intent-gate-service persists
questions to pending-questions.md before dispatching (reusing intent-gate's
register_question contract function), and group replies land in _review/inbox/
(reusing file_inbound_reply). Collecting answers, injecting-and-closing them, and
readiness self-checks always happen on the intent-gate side. DingTalk is only the
transport layer; the file ledger does not depend on it to survive.
Why the split
- It blocks the user:
ask_humanblocks waiting for a human reply, and blows up if the MCP client's tool timeout is misconfigured; people who only want intent alignment shouldn't pay that cost. - It's heavy: fastapi / uvicorn / cryptography / httpx (+ optional dingtalk-stream) are all DingTalk-only dependencies the main plugin should never carry.
- Fault isolation: wrong DingTalk credentials, a dead callback, or nobody answering in the group — none of these affect the main plugin's alignment loop.
When to install it
- Intent gaps should be ruled on by business/technical roles, not guessed by the
person at the chat box: you want
dispatch_questionitems from requirement analysis posted to a DingTalk group @ the corresponding role (📋 business / 🔧 technical), with replies auto-persisted to the inbox for intent-gate to collect and close → install intent-gate-service and usegroup_dispatch. - You hit an irreversible operation or a red-light decision mid-coding and need
someone in the group to rule immediately:
ask_humanblocks waiting for an allowlisted member's reply, and falls back to the conservative path on timeout. - Neither applies: don't install it — intent-gate alone is a complete closed loop (chat-dialog fallback). This service is fully optional; removing or never installing it does not affect any main-plugin capability.
MCP tool surface
| Tool | Description |
|---|---|
ask_human(question, context, options, timeout_sec) | 🔴 Blocking decision gate: posts to the group and waits for an allowlisted member's reply; on timeout returns a NO_REPLY fallback string (raise the client tool timeout, ≥30 min recommended) |
list_pending() | List pending gates |
cancel_gate(token) | Cancel a pending gate |
group_dispatch(feature, gap, category, options, ...) | Dispatch an intent-alignment question to the group (non-blocking, returns immediately; persisted before sending; 📋 business / 🔧 technical decides who gets @'d) |
group_rebroadcast(feature) | Re-post a summary of unresolved questions to the group (nudging after session recovery) |
Companion flow: after group replies are persisted, use intent-gate's
collect_answers to collect them, resolve_question to close them into the ledger,
and list_pending_questions for the readiness self-check.
Quick start
⚠️ The main plugin comes first — without it this service cannot even start. intent-gate-service imports intent-gate's contract layer (
register_question/file_inbound_reply) and shares its file ledger; and without the intent-gate plugin mounted in your agent, dispatched questions have nobody to collect or close them.
# 1) Install the main plugin (contract layer) + this service into the same env
pipx install intent-gate-mcp
pipx inject intent-gate-mcp intent-gate-service # stream inbound fallback: pipx inject intent-gate-mcp "intent-gate-service[stream]"
# 2) Clone this repo (you need .env for credentials anyway)
git clone https://github.com/baixinghao/intent-gate-service.git
cd intent-gate-service
# 3) Credentials + self-check
cp .env.example .env # fill in DingTalk credentials and the allowlist
python -m unittest discover -s tests -v # offline tests (fake HTTP layer)
intent-gate-service # stdio MCP + DingTalk inbound
Mounted alongside intent-gate (Claude Code .mcp.json):
{
"mcpServers": {
"intent-gate": { "command": "intent-gate" },
"intent-gate-service": { "command": "intent-gate-service" }
}
}
Note: an MCP client's default tool timeout may be shorter than a human's response time (the decision-gate scenario). Raise the tool timeout for
intent-gate-service(≥ 30 minutes recommended). SSE exposure:intent-gate-service --mcp-transport sse --mcp-port 8400; event-stream subscription viaGET /events(SSE, requires a Bearer token or loopback-only).
Using it: what to say
| You say / the situation | Tool | What happens |
|---|---|---|
| "这道业务题发钉钉群问"(分析中遇到 📋/🔧 断层) | group_dispatch | Persisted to pending-questions.md first, then posted to the group @ the right role; returns immediately (non-blocking) |
群里回复:@机器人 [HG-XXXX] 选1 | inbound → intent-gate | Signature-verified, decrypted, persisted verbatim to _review/inbox/; intent-gate's collect_answers picks it up next turn |
| "催一下单" / 会话恢复后 | group_rebroadcast | Re-posts a summary of unresolved questions to the group |
| 🔴 红灯决策 / 不可逆操作(agent 自发升级) | ask_human | Posts to the group and blocks waiting for an allowlisted member's reply; on timeout returns NO_REPLY and takes the conservative path |
One rule to remember: answers are always collected and settled on the intent-gate side — this service only dispatches and delivers. DingTalk is just the transport; the file ledger never depends on it.
DingTalk-side configuration
Outbound (sending messages to the group) — pick ONE
⚠️ Webhook is deprecated: DingTalk officially announced the custom-bot product is being retired — since 2023-09 no group can create new ones (existing ones still work for now). New projects should use B directly; A exists only for legacy compatibility and logs a DEPRECATED warning at runtime when configured. When the api configuration is complete it takes precedence, regardless of whether a webhook is also configured.
- B. Enterprise internal-app bot (recommended, the official replacement) — free,
and its QPS limits are irrelevant for this scenario:
- Have an org admin grant you "developer permission" in the developer console (for personal use you can create a free org of your own, where you are the admin yourself)
- In the developer console create an enterprise internal app → add the "bot" capability → publish
- Pull the bot into the target group
- Obtain
AppKey/AppSecret/robotCode, and the group'sopenConversationId(after the bot joins the group, @ it once and read theconversationIdfrom the callback) - To @ specific people, pass DingTalk userIds via
group_dispatch(..., at_user_ids=[...])
- A. Group custom bot (legacy compatibility only, do not use for new projects): group settings → bots → custom → signing → obtain webhook + secret
Inbound (receiving @ replies) — pick ONE
- http (recommended, zero long connections): enterprise internal app → set the
bot's message-receiving URL to
https://<your-public-address>/dingtalk/callback, and fill the console-provided aes_key/token intoHG_CALLBACK_AES_KEY/HG_CALLBACK_TOKEN(requires a publicly reachable address; NAT traversal works); setHG_INBOUND_MODE=http - stream (fallback):
pip install -e ".[stream]",HG_INBOUND_MODE=stream. Needs no public address, but uses DingTalk's proprietary long connection (DingTalk offers no SSE push — this is the only no-public-URL option)
Decision gates: emergency human escalation during coding
When the agent hits a confidence red light mid-execution, it posts to the group for help (and blocks waiting for a reply):
## 🔴 [HG-7F3A] Agent confidence red light
Question: Delete the old partitions of the production database directly?
Options: 1. Back up first, then delete 2. Delete directly 3. Skip
Reply format: @bot [HG-7F3A] your decision
An allowlisted member replies @bot [HG-7F3A] pick 1 → the agent immediately gets
HUMAN_REPLY[Zhang San]: pick 1 and continues. When there is only one pending gate,
the token may be omitted (HG_ALLOW_IMPLICIT_SINGLE_MATCH).
Security model (fail-closed by default)
- An empty allowlist = nobody can answer a gate
- Replies must carry the
[HG-XXXX]correlation token - HTTP callbacks enforce DingTalk signature verification + AES decryption + timestamp freshness (300s window); per-sender rate limiting
/eventsSSE is loopback-only unlessHG_EVENTS_TOKENis configured- Decision gates return a
NO_REPLYstring on timeout, instructing the agent to take the conservative path
Project structure
src/intent_gate_service/
├── config.py # HG_* env vars (credentials required, fail-fast)
├── gate.py # GateManager: gate correlation, Futures, timeouts, event bus
├── server.py # MCP tool surface (3 gate tools + 2 group-channel tools)
├── bridge.py # group-channel bridge: group_dispatch/group_rebroadcast/build_intake
│ # (persistence reuses intent_gate contract functions; no second implementation)
├── __main__.py # entrypoint (stdio/SSE MCP + resident DingTalk inbound)
└── dingtalk/
├── client.py # outbound: enterprise-bot API (recommended) / webhook (legacy) + @people
├── crypto.py # callback AES-256-CBC encrypt/decrypt + signing
└── inbound/{http,stream}.py # the two inbound transports + SSE event stream
skills/dingtalk-escalation/ # DingTalk escalation protocol (token protocol / allowlist / channels)
File-by-file documentation and the full cross-repo workflow: see intent-gate/docs/STRUCTURE.md.