Skip to main content

intent-gate-service

English | 简体中文

License: MIT Python >=3.11 MCP

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_human blocks 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_question items 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 use group_dispatch.
  • You hit an irreversible operation or a red-light decision mid-coding and need someone in the group to rule immediately: ask_human blocks 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 via GET /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:
    1. 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)
    2. In the developer console create an enterprise internal app → add the "bot" capability → publish
    3. Pull the bot into the target group
    4. Obtain AppKey / AppSecret / robotCode, and the group's openConversationId (after the bot joins the group, @ it once and read the conversationId from the callback)
    5. 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 into HG_CALLBACK_AES_KEY / HG_CALLBACK_TOKEN (requires a publicly reachable address; NAT traversal works); set HG_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
  • /events SSE is loopback-only unless HG_EVENTS_TOKEN is configured
  • Decision gates return a NO_REPLY string 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

MIT

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

intent_gate_service-0.2.1.tar.gz (39.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

intent_gate_service-0.2.1-py3-none-any.whl (31.9 kB view details)

Uploaded Python 3

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

Hashes for intent_gate_service-0.2.1.tar.gz
Algorithm Hash digest
SHA256 6cd432ba217398d3ed5594174db57c03dc064e58f73c4d8e0b869d3011cc58f4
MD5 04e699546d22c18cf929800c52e0e9b6
BLAKE2b-256 cc8f2d6815f5aee8400970b74e7e784040d207456a1d268dde3746fab3c4b97d

See more details on using hashes here.

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

Hashes for intent_gate_service-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 ac50baf65ed667fd48e6412945c04b8fc20bca3e78857c2068aad4c5d38f969c
MD5 fc10aa6f675ce6465f825c093555d8bd
BLAKE2b-256 f74f08c84ff789ea56b08cad2a20609088230dcf47e7efe18daf595afadb14f6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page