Metadata-Version: 2.5
Name: intent-gate-service
Version: 0.2.1
Summary: DingTalk sister MCP service of intent-gate: blocking human-decision gates + group consensus channel for requirement intent alignment
Project-URL: Homepage, https://github.com/baixinghao/intent-gate-service
Project-URL: Repository, https://github.com/baixinghao/intent-gate-service
Project-URL: Issues, https://github.com/baixinghao/intent-gate-service/issues
Project-URL: Main plugin (intent-gate), https://github.com/baixinghao/intent-gate
Author: baixinghao
License: MIT
License-File: LICENSE
Keywords: decision-gate,dingtalk,group-consensus,human-in-the-loop,intent-alignment,mcp
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.11
Requires-Dist: cryptography>=42.0
Requires-Dist: fastapi>=0.110.0
Requires-Dist: httpx>=0.27.0
Requires-Dist: intent-gate-mcp>=0.4.0
Requires-Dist: mcp<2.0,>=1.10.0
Requires-Dist: pydantic-settings>=2.2
Requires-Dist: pydantic>=2.6
Requires-Dist: uvicorn>=0.29.0
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Provides-Extra: stream
Requires-Dist: dingtalk-stream>=0.20.0; extra == 'stream'
Description-Content-Type: text/markdown

# intent-gate-service

**English** | [简体中文](README.zh-CN.md)

[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Python >=3.11](https://img.shields.io/badge/python-%3E%3D3.11-blue)](https://www.python.org/)
[![MCP](https://img.shields.io/badge/protocol-MCP-green)](https://modelcontextprotocol.io/)

**The DingTalk-interaction sister project of
[intent-gate](https://github.com/baixinghao/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.

```bash
# 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`):

```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](https://open-dev.dingtalk.com/) 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](https://github.com/baixinghao/intent-gate/blob/main/docs/STRUCTURE.md).

## License

[MIT](LICENSE)
