intent-gate-service
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.
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file intent_gate_service-0.2.1.tar.gz.
File metadata
- Download URL: intent_gate_service-0.2.1.tar.gz
- Upload date:
- Size: 39.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6cd432ba217398d3ed5594174db57c03dc064e58f73c4d8e0b869d3011cc58f4
|
|
| MD5 |
04e699546d22c18cf929800c52e0e9b6
|
|
| BLAKE2b-256 |
cc8f2d6815f5aee8400970b74e7e784040d207456a1d268dde3746fab3c4b97d
|
File details
Details for the file intent_gate_service-0.2.1-py3-none-any.whl.
File metadata
- Download URL: intent_gate_service-0.2.1-py3-none-any.whl
- Upload date:
- Size: 31.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.21 {"installer":{"name":"uv","version":"0.11.21","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":null,"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ac50baf65ed667fd48e6412945c04b8fc20bca3e78857c2068aad4c5d38f969c
|
|
| MD5 |
fc10aa6f675ce6465f825c093555d8bd
|
|
| BLAKE2b-256 |
f74f08c84ff789ea56b08cad2a20609088230dcf47e7efe18daf595afadb14f6
|