@tmim/dsh-plugin-tmim 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +124 -3
- package/README.zh-CN.md +118 -0
- package/dist/index.d.mts +33 -0
- package/dist/index.mjs +625 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +64 -6
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 beyondalbert
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,124 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
# @tmim/dsh-plugin-tmim
|
|
2
|
+
|
|
3
|
+
A [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) plugin that
|
|
4
|
+
connects the harness to the **TM IM** network. It turns TM IM into a message
|
|
5
|
+
channel for a Harness agent:
|
|
6
|
+
|
|
7
|
+
- **Inbound**: TM IM Agent Takeover **Webhook** events (`message.created`,
|
|
8
|
+
`mention.created`) are signature-verified, deduplicated, and fed into a
|
|
9
|
+
Harness agent turn.
|
|
10
|
+
- **Outbound**: the agent's assistant replies are relayed back to the originating
|
|
11
|
+
TM IM conversation.
|
|
12
|
+
- **Tools**: the agent can proactively send messages, create/search conversations,
|
|
13
|
+
and pull history over the TM IM REST API.
|
|
14
|
+
|
|
15
|
+
Protocol: https://tmim.stkg.ltd/about/integration — a copy of the extracted
|
|
16
|
+
protocol contract ships in this repo as [`docs-extra/tmim-integration.md`](docs-extra/tmim-integration.md).
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Requirements
|
|
21
|
+
|
|
22
|
+
- Node.js ≥ 20 (Harness recommends ≥ 24)
|
|
23
|
+
- A running [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) host
|
|
24
|
+
- A TM IM account with **Agent Takeover** enabled and an **api_key** (recommended)
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
# inside your harness workspace
|
|
30
|
+
npm install @tmim/dsh-plugin-tmim
|
|
31
|
+
# or, from this repo
|
|
32
|
+
pnpm install && pnpm build
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Add the plugin to your Cordis composition (`cordis.yml`):
|
|
36
|
+
|
|
37
|
+
```yaml
|
|
38
|
+
- name: '@tmim/dsh-plugin-tmim'
|
|
39
|
+
config:
|
|
40
|
+
baseUrl: 'https://tmim.example.com'
|
|
41
|
+
credential:
|
|
42
|
+
kind: 'apikey' # or: kind: 'password', username, password
|
|
43
|
+
token: 'tm_xxxxxxxx'
|
|
44
|
+
webhook:
|
|
45
|
+
enabled: true
|
|
46
|
+
path: '/tm/callback'
|
|
47
|
+
secret: 'whsec_...' # issued by TM IM when WEBHOOK push mode enabled
|
|
48
|
+
agent:
|
|
49
|
+
provider: 'deepseek-official'
|
|
50
|
+
model: 'deepseek-chat'
|
|
51
|
+
enableAgentTakeover: true
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Configuration
|
|
55
|
+
|
|
56
|
+
| Field | Type | Default | Description |
|
|
57
|
+
|---|---|---|---|
|
|
58
|
+
| `baseUrl` | string | — | TM IM base URL, no `/v1` suffix. |
|
|
59
|
+
| `credential.kind` | `'apikey' \| 'password'` | — | Authentication mode. |
|
|
60
|
+
| `credential.token` | string | — | Agent Takeover api_key (when `apikey`). |
|
|
61
|
+
| `credential.username`/`password` | string | — | TM IM login (when `password`). |
|
|
62
|
+
| `deviceId` | string | `dsh-<pid>` | Reported via `X-TM-Device-Id`. |
|
|
63
|
+
| `enableAgentTakeover` | boolean | `false` | POST takeover config on startup. |
|
|
64
|
+
| `webhook.enabled` | boolean | `false` | Mount the webhook receiver. |
|
|
65
|
+
| `webhook.path` | string | `/tm/callback` | URL path; must be public HTTPS. |
|
|
66
|
+
| `webhook.secret` | string | `''` | `webhook_secret` (required when enabled). |
|
|
67
|
+
| `webhook.maxSkewSeconds` | number | `300` | Replay-protection clock skew. |
|
|
68
|
+
| `agent.provider` / `agent.model` | string | — | Model the bridged agent turn uses. |
|
|
69
|
+
| `tools` | boolean | `true` | Register proactive TM IM tools. |
|
|
70
|
+
| `relay` | boolean | `true` | Relay assistant replies to TM IM. |
|
|
71
|
+
|
|
72
|
+
> ⚠️ The `webhook_secret` is shown **only once** by TM IM (at WEBHOOK enable
|
|
73
|
+
> time or on explicit rotation). Do not rely on re-reading it from the API.
|
|
74
|
+
|
|
75
|
+
## How the bridge works
|
|
76
|
+
|
|
77
|
+
1. TM IM POSTs an event to `webhook.path` with an `X-TM-Signature`.
|
|
78
|
+
2. The receiver verifies `HMAC-SHA256(secret, "<ts>.<rawBody>")` and freshness,
|
|
79
|
+
then returns `200` immediately (the 5-second budget is only for the HTTP
|
|
80
|
+
return, not for LLM work).
|
|
81
|
+
3. A unique Harness session per TM IM conversation (`tmim:<convId>`) is created
|
|
82
|
+
and the inbound text is fed via `agent.followup(createUserMessage(...))`.
|
|
83
|
+
4. `session/event` `assistant/message` events are observed and the final text
|
|
84
|
+
reply is sent back to the conversation with `POST /v1/conversations/{id}/messages`.
|
|
85
|
+
|
|
86
|
+
Restart-safe resume uses the same session id, so conversation context carries
|
|
87
|
+
across restarts.
|
|
88
|
+
|
|
89
|
+
## Tools
|
|
90
|
+
|
|
91
|
+
Registered when `tools: true`:
|
|
92
|
+
|
|
93
|
+
| Tool | Purpose |
|
|
94
|
+
|---|---|
|
|
95
|
+
| `tmim_send_text` | Send a text message to a conversation (Markdown OK). |
|
|
96
|
+
| `tmim_get_or_create_direct_conversation` | Resolve a peer handle to a `convId`. |
|
|
97
|
+
| `tmim_create_group_conversation` | Create a group. |
|
|
98
|
+
| `tmim_list_conversations` | List recent conversations. |
|
|
99
|
+
| `tmim_pull_messages` | Incremental `since_seq` pull of a conversation. |
|
|
100
|
+
|
|
101
|
+
## Webhook verification (self test)
|
|
102
|
+
|
|
103
|
+
The signature logic is verified against TM IM's documented test vector (6 cases: HMAC self-test vector, valid verification, mismatched digest, stale/replayed timestamp, malformed header, raw-body sensitivity). Inside the harness workspace:
|
|
104
|
+
|
|
105
|
+
```sh
|
|
106
|
+
cd _harness/deepseek-harness-master/packages/tmim/dsh-plugin-tmim
|
|
107
|
+
pnpm test
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
> The package ships its own `vitest.config.ts` because the monorepo root config
|
|
111
|
+
> drives a workspace project topology that only discovers tests under
|
|
112
|
+
> `packages/*/*/tests/**`; running from the plugin directory keeps the signature
|
|
113
|
+
> test self-contained on `node:crypto` + `src/*` (no repo path aliases).
|
|
114
|
+
|
|
115
|
+
## Security notes
|
|
116
|
+
|
|
117
|
+
- Never log the webhook secret or api key.
|
|
118
|
+
- The webhook handler uses the **raw request body bytes** for verification — do
|
|
119
|
+
not re-serialize JSON.
|
|
120
|
+
- An HTTPS, publicly reachable `endpoint_url` is mandatory for production.
|
|
121
|
+
|
|
122
|
+
## License
|
|
123
|
+
|
|
124
|
+
MIT
|
package/README.zh-CN.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# @tmim/dsh-plugin-tmim
|
|
2
|
+
|
|
3
|
+
一个 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 插件,用于将
|
|
4
|
+
Harness 接入 **TM IM** 网络。它把 TM IM 变成 Harness 智能体的消息通道:
|
|
5
|
+
|
|
6
|
+
- **入站(Inbound)**:TM IM **Agent Takeover** **Webhook** 事件(`message.created`、
|
|
7
|
+
`mention.created`)经过签名校验、去重后,喂给 Harness 智能体的一次作答(turn)。
|
|
8
|
+
- **出站(Outbound)**:智能体(assistant)的回复被转发回发起对话的 TM IM 会话。
|
|
9
|
+
- **工具(Tools)**:智能体可以主动通过 TM IM REST API 发送消息、创建/查找会话、
|
|
10
|
+
拉取历史消息。
|
|
11
|
+
|
|
12
|
+
协议文档:https://tmim.stkg.ltd/about/integration —— 本仓库也附带了提取出来的协议
|
|
13
|
+
契约副本:[`docs-extra/tmim-integration.md`](docs-extra/tmim-integration.md)。
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 运行环境要求
|
|
18
|
+
|
|
19
|
+
- Node.js ≥ 20(Harness 建议 ≥ 24)
|
|
20
|
+
- 一个正在运行的 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 宿主
|
|
21
|
+
- 一个已开启 **Agent Takeover** 且带有 **api_key**(推荐)的 TM IM 账号
|
|
22
|
+
|
|
23
|
+
## 安装
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
# 在你的 harness 工作区中
|
|
27
|
+
npm install @tmim/dsh-plugin-tmim
|
|
28
|
+
# 或者,从本仓库源码安装
|
|
29
|
+
pnpm install && pnpm build
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
将插件添加到你的 Cordis 组合配置(`cordis.yml`)中:
|
|
33
|
+
|
|
34
|
+
```yaml
|
|
35
|
+
- name: '@tmim/dsh-plugin-tmim'
|
|
36
|
+
config:
|
|
37
|
+
baseUrl: 'https://tmim.example.com'
|
|
38
|
+
credential:
|
|
39
|
+
kind: 'apikey' # 或: kind: 'password', username, password
|
|
40
|
+
token: 'tm_xxxxxxxx'
|
|
41
|
+
webhook:
|
|
42
|
+
enabled: true
|
|
43
|
+
path: '/tm/callback'
|
|
44
|
+
secret: 'whsec_...' # 启用 WEBHOOK 推送模式时由 TM IM 签发
|
|
45
|
+
agent:
|
|
46
|
+
provider: 'deepseek-official'
|
|
47
|
+
model: 'deepseek-chat'
|
|
48
|
+
enableAgentTakeover: true
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 配置项
|
|
52
|
+
|
|
53
|
+
| 字段 | 类型 | 默认值 | 说明 |
|
|
54
|
+
|---|---|---|---|
|
|
55
|
+
| `baseUrl` | string | — | TM IM 基础地址,不要带 `/v1` 后缀。 |
|
|
56
|
+
| `credential.kind` | `'apikey' \| 'password'` | — | 认证方式。 |
|
|
57
|
+
| `credential.token` | string | — | Agent Takeover 的 api_key(`apikey` 方式时)。 |
|
|
58
|
+
| `credential.username`/`password` | string | — | TM IM 登录账号/密码(`password` 方式时)。 |
|
|
59
|
+
| `deviceId` | string | `dsh-<pid>` | 通过 `X-TM-Device-Id` 上报。 |
|
|
60
|
+
| `enableAgentTakeover` | boolean | `false` | 启动时是否 POST takeover 配置。 |
|
|
61
|
+
| `webhook.enabled` | boolean | `false` | 是否挂载 webhook 接收器。 |
|
|
62
|
+
| `webhook.path` | string | `/tm/callback` | URL 路径;必须是可公网访问的 HTTPS 地址。 |
|
|
63
|
+
| `webhook.secret` | string | `''` | `webhook_secret`(启用时必填)。 |
|
|
64
|
+
| `webhook.maxSkewSeconds` | number | `300` | 防重放的时钟偏移上限。 |
|
|
65
|
+
| `agent.provider` / `agent.model` | string | — | 桥接智能体作答所使用的模型。 |
|
|
66
|
+
| `tools` | boolean | `true` | 是否注册主动式的 TM IM 工具。 |
|
|
67
|
+
| `relay` | boolean | `true` | 是否将智能体回复转发回 TM IM。 |
|
|
68
|
+
|
|
69
|
+
> ⚠️ `webhook_secret` 只会由 TM IM **展示一次**(在启用 WEBHOOK 时或显式轮换时)。
|
|
70
|
+
> 不要依赖通过 API 重新读取它。
|
|
71
|
+
|
|
72
|
+
## 桥接工作原理
|
|
73
|
+
|
|
74
|
+
1. TM IM 以带 `X-TM-Signature` 的请求向 `webhook.path` POST 一个事件。
|
|
75
|
+
2. 接收器校验 `HMAC-SHA256(secret, "<ts>.<rawBody>")` 以及时间新鲜度,然后立刻返回
|
|
76
|
+
`200`(那 5 秒预算只用于返回 HTTP 响应,而不是用于 LLM 的处理)。
|
|
77
|
+
3. 每个 TM IM 会话对应一个唯一的 Harness 会话(`tmim:<convId>`),入站文本通过
|
|
78
|
+
`agent.followup(createUserMessage(...))` 送入。
|
|
79
|
+
4. 监听 `session/event` 的 `assistant/message` 事件,并把最终文本回复通过
|
|
80
|
+
`POST /v1/conversations/{id}/messages` 发回会话。
|
|
81
|
+
|
|
82
|
+
使用相同的会话 id 可以做到重启后恢复,因此会话上下文在重启后仍然保留。
|
|
83
|
+
|
|
84
|
+
## 工具
|
|
85
|
+
|
|
86
|
+
当 `tools: true` 时注册以下工具:
|
|
87
|
+
|
|
88
|
+
| 工具 | 用途 |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `tmim_send_text` | 向会话发送文本消息(支持 Markdown)。 |
|
|
91
|
+
| `tmim_get_or_create_direct_conversation` | 将对方 handle 解析为 `convId`。 |
|
|
92
|
+
| `tmim_create_group_conversation` | 创建群组。 |
|
|
93
|
+
| `tmim_list_conversations` | 列出最近的会话。 |
|
|
94
|
+
| `tmim_pull_messages` | 基于 `since_seq` 增量拉取会话消息。 |
|
|
95
|
+
|
|
96
|
+
## Webhook 校验(自测)
|
|
97
|
+
|
|
98
|
+
签名逻辑通过 TM IM 文档中的测试向量进行验证(共 6 个用例:HMAC 自测向量、有效校验、
|
|
99
|
+
摘要不匹配、过期/重放的时间戳、格式错误的头、原始请求体敏感度)。在 harness 工作区中:
|
|
100
|
+
|
|
101
|
+
```sh
|
|
102
|
+
cd _harness/deepseek-harness-master/packages/tmim/dsh-plugin-tmim
|
|
103
|
+
pnpm test
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
> 本包自带 `vitest.config.ts`,因为 monorepo 根配置驱动的工作区工程拓扑只会发现
|
|
107
|
+
> `packages/*/*/tests/**` 下的测试;从插件目录运行可以让签名测试基于
|
|
108
|
+
> `node:crypto` + `src/*` 独立进行(无需仓库路径别名)。
|
|
109
|
+
|
|
110
|
+
## 安全注意事项
|
|
111
|
+
|
|
112
|
+
- 永远不要记录(log)webhook secret 或 api key。
|
|
113
|
+
- webhook 处理器使用**原始请求体字节**进行校验——不要重新序列化 JSON。
|
|
114
|
+
- 生产环境必须使用 HTTPS 且公网可达的 `endpoint_url`。
|
|
115
|
+
|
|
116
|
+
## 许可证
|
|
117
|
+
|
|
118
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Context } from "@deepseek-ai/cordis";
|
|
2
|
+
export interface WebhookConfig {
|
|
3
|
+
enabled: boolean;
|
|
4
|
+
path: string;
|
|
5
|
+
secret: string;
|
|
6
|
+
maxSkewSeconds: number;
|
|
7
|
+
}
|
|
8
|
+
export interface AgentConfig {
|
|
9
|
+
provider: string;
|
|
10
|
+
model: string;
|
|
11
|
+
}
|
|
12
|
+
export type Credential = {
|
|
13
|
+
kind: 'apikey';
|
|
14
|
+
token: string;
|
|
15
|
+
} | {
|
|
16
|
+
kind: 'password';
|
|
17
|
+
username: string;
|
|
18
|
+
password: string;
|
|
19
|
+
};
|
|
20
|
+
export type Config = {
|
|
21
|
+
baseUrl: string;
|
|
22
|
+
credential: Credential;
|
|
23
|
+
deviceId: string;
|
|
24
|
+
webhook: WebhookConfig;
|
|
25
|
+
enableAgentTakeover: boolean;
|
|
26
|
+
agent: AgentConfig;
|
|
27
|
+
cwd: string;
|
|
28
|
+
tools: boolean;
|
|
29
|
+
};
|
|
30
|
+
export declare const Config: Config;
|
|
31
|
+
export declare const name = "tmim";
|
|
32
|
+
export declare function apply(ctx: Context, config: Config): Promise<void>;
|
|
33
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { createHmac, randomUUID, timingSafeEqual } from "node:crypto";
|
|
3
|
+
import { createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
4
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
5
|
+
// src/tmim-client.ts
|
|
6
|
+
var TmimError = class extends Error {
|
|
7
|
+
code;
|
|
8
|
+
httpStatus;
|
|
9
|
+
payload;
|
|
10
|
+
constructor(code, message, httpStatus, payload) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.httpStatus = httpStatus;
|
|
14
|
+
this.payload = payload;
|
|
15
|
+
this.name = "TmimError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
var TmimClient = class {
|
|
19
|
+
config;
|
|
20
|
+
token;
|
|
21
|
+
fetchImpl;
|
|
22
|
+
timeoutMs;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.token = config.token;
|
|
26
|
+
this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis);
|
|
27
|
+
this.timeoutMs = config.timeoutMs ?? 15e3;
|
|
28
|
+
}
|
|
29
|
+
/** Current bearer token (may be a JWT or an Agent api_key). */
|
|
30
|
+
get authToken() {
|
|
31
|
+
return this.token;
|
|
32
|
+
}
|
|
33
|
+
async request(method, path, body, opts = {}) {
|
|
34
|
+
const url = `${this.config.baseUrl}/v1${path}`;
|
|
35
|
+
const headers = {
|
|
36
|
+
Accept: "application/json",
|
|
37
|
+
...opts.headers ?? {}
|
|
38
|
+
};
|
|
39
|
+
if (body !== void 0) headers["Content-Type"] = "application/json; charset=utf-8";
|
|
40
|
+
if (this.config.deviceId) headers["X-TM-Device-Id"] = this.config.deviceId;
|
|
41
|
+
if (opts.auth !== false && this.token) headers["Authorization"] = `Bearer ${this.token}`;
|
|
42
|
+
const controller = new AbortController();
|
|
43
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
44
|
+
try {
|
|
45
|
+
const res = await this.fetchImpl(url, {
|
|
46
|
+
method,
|
|
47
|
+
headers,
|
|
48
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
49
|
+
signal: controller.signal
|
|
50
|
+
});
|
|
51
|
+
let envelope;
|
|
52
|
+
let text = "";
|
|
53
|
+
try {
|
|
54
|
+
text = await res.text();
|
|
55
|
+
envelope = JSON.parse(text);
|
|
56
|
+
} catch {
|
|
57
|
+
envelope = void 0;
|
|
58
|
+
}
|
|
59
|
+
if (res.status === 401 || res.status === 403) throw new TmimError(res.status, `tmim auth failed (HTTP ${res.status})`, res.status, text);
|
|
60
|
+
if (!res.ok && !envelope) throw new TmimError(0, `tmim HTTP ${res.status}: ${text.slice(0, 300)}`, res.status, text);
|
|
61
|
+
if (envelope && envelope.code !== 0) throw new TmimError(envelope.code, envelope.message ?? `tmim error ${envelope.code}`, res.status, envelope);
|
|
62
|
+
return envelope?.data ?? void 0;
|
|
63
|
+
} finally {
|
|
64
|
+
clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Login with handle/password; stores the returned JWT. */
|
|
68
|
+
async login(handle, password) {
|
|
69
|
+
const data = await this.request("POST", "/auth/login", {
|
|
70
|
+
handle,
|
|
71
|
+
password
|
|
72
|
+
}, { auth: false });
|
|
73
|
+
this.token = data.token;
|
|
74
|
+
return {
|
|
75
|
+
token: data.token,
|
|
76
|
+
refreshToken: data.refresh_token
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** Exchange a refresh token for a new access token (refresh_token is one-time). */
|
|
80
|
+
async refresh(refreshToken) {
|
|
81
|
+
const data = await this.request("POST", "/auth/refresh", { refresh_token: refreshToken }, { auth: false });
|
|
82
|
+
this.token = data.token;
|
|
83
|
+
return {
|
|
84
|
+
token: data.token,
|
|
85
|
+
refreshToken: data.refresh_token
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** Set an explicit bearer token (e.g. an Agent takeover api_key). */
|
|
89
|
+
setToken(token) {
|
|
90
|
+
this.token = token;
|
|
91
|
+
return this;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Configure Agent Takeover.
|
|
95
|
+
* doc: POST /v1/me/agent-takeover
|
|
96
|
+
* Returns the issued webhook_secret ONLY when the merged push mode is
|
|
97
|
+
* WEBHOOK and no secret existed yet (shown exactly once).
|
|
98
|
+
*/
|
|
99
|
+
async configureAgentTakeover(args) {
|
|
100
|
+
const body = { push_mode: args.pushMode };
|
|
101
|
+
if (args.endpointUrl !== void 0) body.endpoint_url = args.endpointUrl;
|
|
102
|
+
if (args.capabilities !== void 0) body.capabilities = args.capabilities;
|
|
103
|
+
if (args.modelInfo !== void 0) body.model_info = args.modelInfo;
|
|
104
|
+
if (args.rateLimit !== void 0) body.rate_limit = args.rateLimit;
|
|
105
|
+
return this.request("POST", "/me/agent-takeover", body);
|
|
106
|
+
}
|
|
107
|
+
/** Rotate the webhook secret; returns the new plaintext exactly once. */
|
|
108
|
+
async rotateWebhookSecret() {
|
|
109
|
+
return this.request("POST", "/me/agent-takeover/rotate-webhook-secret");
|
|
110
|
+
}
|
|
111
|
+
/** Disable Agent Takeover entirely. */
|
|
112
|
+
async disableAgentTakeover() {
|
|
113
|
+
await this.request("DELETE", "/me/agent-takeover");
|
|
114
|
+
}
|
|
115
|
+
/** Read current takeover config (never includes secrets). */
|
|
116
|
+
async getAgentTakeoverStatus() {
|
|
117
|
+
const d = await this.request("GET", "/me/agent-takeover");
|
|
118
|
+
return {
|
|
119
|
+
actorId: d.actor_id,
|
|
120
|
+
enabled: d.enabled,
|
|
121
|
+
pushMode: d.push_mode,
|
|
122
|
+
...d.endpoint_url === void 0 ? {} : { endpointUrl: d.endpoint_url },
|
|
123
|
+
hasApiKey: d.has_api_key,
|
|
124
|
+
hasWebhookSecret: d.has_webhook_secret
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/** Get or create a direct (1:1) conversation with a peer. */
|
|
128
|
+
async getOrCreateDirectConversation(peer) {
|
|
129
|
+
const d = await this.request("POST", "/conversations/direct", { peer });
|
|
130
|
+
return {
|
|
131
|
+
convId: d.conv_id,
|
|
132
|
+
convType: d.conv_type,
|
|
133
|
+
peer: d.peer,
|
|
134
|
+
created: d.created,
|
|
135
|
+
lastSeq: d.last_seq
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** Create a group conversation. `members` excludes the caller. */
|
|
139
|
+
async createGroupConversation(title, members) {
|
|
140
|
+
return { convId: (await this.request("POST", "/conversations/group", {
|
|
141
|
+
title,
|
|
142
|
+
members
|
|
143
|
+
})).conv_id };
|
|
144
|
+
}
|
|
145
|
+
/** List the caller's conversations (cursor paginated). */
|
|
146
|
+
async listConversations(limit = 50, cursor) {
|
|
147
|
+
const q = new URLSearchParams({ limit: String(limit) });
|
|
148
|
+
if (cursor) q.set("cursor", cursor);
|
|
149
|
+
const d = await this.request("GET", `/conversations?${q}`);
|
|
150
|
+
return {
|
|
151
|
+
items: d.items,
|
|
152
|
+
nextCursor: d.next_cursor
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Send a text message. `clientMsgId` is the idempotency key; re-sending the
|
|
157
|
+
* same key returns the identical original message (no duplicate).
|
|
158
|
+
*/
|
|
159
|
+
async sendText(convId, text, clientMsgId, replyTo) {
|
|
160
|
+
const d = await this.request("POST", `/conversations/${convId}/messages`, {
|
|
161
|
+
msg_type: "TEXT",
|
|
162
|
+
client_msg_id: clientMsgId,
|
|
163
|
+
content: { text },
|
|
164
|
+
...replyTo === void 0 ? {} : { reply_to: replyTo }
|
|
165
|
+
});
|
|
166
|
+
return {
|
|
167
|
+
messageId: d.message_id,
|
|
168
|
+
convId: d.conv_id,
|
|
169
|
+
seq: d.seq,
|
|
170
|
+
clientMsgId: d.client_msg_id,
|
|
171
|
+
senderId: d.sender_id,
|
|
172
|
+
createdAt: d.created_at
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
/** Incremental pull: messages with seq > sinceSeq, ascending. */
|
|
176
|
+
async pullMessagesSince(convId, sinceSeq, limit = 200) {
|
|
177
|
+
const q = new URLSearchParams({
|
|
178
|
+
since_seq: String(sinceSeq),
|
|
179
|
+
limit: String(limit)
|
|
180
|
+
});
|
|
181
|
+
return (await this.request("GET", `/conversations/${convId}/messages?${q}`)).items ?? [];
|
|
182
|
+
}
|
|
183
|
+
/** Cursor pull: newest-first history page. */
|
|
184
|
+
async pullConversationHistory(convId, cursor, limit = 50) {
|
|
185
|
+
const q = new URLSearchParams({ limit: String(limit) });
|
|
186
|
+
if (cursor) q.set("cursor", cursor);
|
|
187
|
+
const d = await this.request("GET", `/conversations/${convId}/messages?${q}`);
|
|
188
|
+
return {
|
|
189
|
+
items: d.items ?? [],
|
|
190
|
+
nextCursor: d.next_cursor,
|
|
191
|
+
hasMore: d.has_more
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
/** Constant-time HMAC-SHA256 verification helper. */
|
|
196
|
+
function hmacSha256(secret, message) {
|
|
197
|
+
return createHmac("sha256", secret).update(message, "utf8").digest("hex");
|
|
198
|
+
}
|
|
199
|
+
/** Verify a webhook request signature and freshness. Exported for reuse and tests. */
|
|
200
|
+
function verifyWebhookSignature(input) {
|
|
201
|
+
const { secret, timestamp, rawBody, signatureHeader } = input;
|
|
202
|
+
if (!secret) return {
|
|
203
|
+
ok: false,
|
|
204
|
+
reason: "missing-secret"
|
|
205
|
+
};
|
|
206
|
+
if (!signatureHeader) return {
|
|
207
|
+
ok: false,
|
|
208
|
+
reason: "missing-header"
|
|
209
|
+
};
|
|
210
|
+
if (!signatureHeader.startsWith("sha256=")) return {
|
|
211
|
+
ok: false,
|
|
212
|
+
reason: "bad-format"
|
|
213
|
+
};
|
|
214
|
+
const provided = signatureHeader.slice(7);
|
|
215
|
+
if (!/^[0-9a-f]{64}$/i.test(provided)) return {
|
|
216
|
+
ok: false,
|
|
217
|
+
reason: "bad-format"
|
|
218
|
+
};
|
|
219
|
+
const ts = Number(timestamp);
|
|
220
|
+
if (!Number.isFinite(ts)) return {
|
|
221
|
+
ok: false,
|
|
222
|
+
reason: "bad-format"
|
|
223
|
+
};
|
|
224
|
+
const now = input.now ?? Math.floor(Date.now() / 1e3);
|
|
225
|
+
const maxSkew = input.maxSkewSeconds ?? 300;
|
|
226
|
+
if (Math.abs(ts - now) > maxSkew) return {
|
|
227
|
+
ok: false,
|
|
228
|
+
reason: "replay"
|
|
229
|
+
};
|
|
230
|
+
const expected = hmacSha256(secret, `${timestamp}.${rawBody.toString("utf8")}`);
|
|
231
|
+
const a = Buffer.from(provided.toLowerCase(), "utf8");
|
|
232
|
+
const b = Buffer.from(expected, "utf8");
|
|
233
|
+
return a.length === b.length && timingSafeEqual(a, b) ? { ok: true } : {
|
|
234
|
+
ok: false,
|
|
235
|
+
reason: "mismatch"
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
// src/webhook.ts
|
|
239
|
+
function readBody(req) {
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
const chunks = [];
|
|
242
|
+
req.on("data", (c) => chunks.push(c));
|
|
243
|
+
req.on("end", () => resolve(Buffer.concat(chunks)));
|
|
244
|
+
req.on("error", reject);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Register the webhook receiver on a Harness `webServer` context.
|
|
249
|
+
* @returns a disposer that unmounts the route.
|
|
250
|
+
*/
|
|
251
|
+
function mountTmimWebhook(ctx, options) {
|
|
252
|
+
const webServer = ctx.webServer;
|
|
253
|
+
const processed = options.processedEventIds ?? /* @__PURE__ */ new Set();
|
|
254
|
+
const seen = /* @__PURE__ */ new Map();
|
|
255
|
+
const handler = async (req, res) => {
|
|
256
|
+
if (req.method !== "POST") {
|
|
257
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
258
|
+
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
const raw = await readBody(req);
|
|
262
|
+
const header = (name) => req.headers[name.toLowerCase()]?.toString();
|
|
263
|
+
const tsHeader = header("x-tm-timestamp");
|
|
264
|
+
const sigHeader = header("x-tm-signature");
|
|
265
|
+
const eventId = header("x-tm-event-id");
|
|
266
|
+
const deliveryAttempt = Number(header("x-tm-delivery-attempt") ?? "1");
|
|
267
|
+
const result = verifyWebhookSignature({
|
|
268
|
+
secret: options.secret,
|
|
269
|
+
timestamp: tsHeader ?? "",
|
|
270
|
+
rawBody: raw,
|
|
271
|
+
signatureHeader: sigHeader,
|
|
272
|
+
...options.maxSkewSeconds === void 0 ? {} : { maxSkewSeconds: options.maxSkewSeconds }
|
|
273
|
+
});
|
|
274
|
+
if (!result.ok || !tsHeader) {
|
|
275
|
+
respond(res, 400, "unauthenticated");
|
|
276
|
+
ctx.logger.warn(`[tmim-webhook] rejected event ${eventId ?? "(no id)"}: ${deserializeResult(result)}`);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const last = seen.get(eventId ?? "");
|
|
280
|
+
if (eventId && last !== void 0 && Number(last) <= deliveryAttempt) {
|
|
281
|
+
respond(res, 200, "duplicate");
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
let event;
|
|
285
|
+
try {
|
|
286
|
+
event = JSON.parse(raw.toString("utf8"));
|
|
287
|
+
} catch {
|
|
288
|
+
respond(res, 400, "bad json");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
if (eventId) seen.set(eventId, String(deliveryAttempt));
|
|
292
|
+
if (eventId) processed.add(eventId);
|
|
293
|
+
respond(res, 200, "ok");
|
|
294
|
+
try {
|
|
295
|
+
await options.handle(event, { deliveryAttempt });
|
|
296
|
+
} catch (error) {
|
|
297
|
+
ctx.logger.error(`[tmim-webhook] handler failed for event ${eventId}: ${String(error)}`);
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const dispose = webServer.register({
|
|
301
|
+
kind: "exact",
|
|
302
|
+
path: options.path,
|
|
303
|
+
handler
|
|
304
|
+
});
|
|
305
|
+
ctx.logger.info(`[tmim-webhook] mounted at ${options.path}`);
|
|
306
|
+
return dispose;
|
|
307
|
+
}
|
|
308
|
+
function deserializeResult(result) {
|
|
309
|
+
return result.ok ? "ok" : result.reason;
|
|
310
|
+
}
|
|
311
|
+
function respond(res, status, statusText) {
|
|
312
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
313
|
+
res.end(JSON.stringify({ status: statusText }));
|
|
314
|
+
}
|
|
315
|
+
// src/bridge.ts
|
|
316
|
+
/**
|
|
317
|
+
* Bridge: turn inbound TM IM events into Harness agent turns, and relay the
|
|
318
|
+
* agent's assistant replies back to the TM IM conversation.
|
|
319
|
+
*
|
|
320
|
+
* One Harness session is maintained per TM IM conversation (`tmim:<convId>`).
|
|
321
|
+
* Inbound messages become `user` messages via `agent.followup(createUserMessage(...))`.
|
|
322
|
+
* Assistant output is observed through `session/event` (`assistant/message`) and
|
|
323
|
+
* sent back through `TmimClient.sendText`.
|
|
324
|
+
*/
|
|
325
|
+
var TmimBridge = class {
|
|
326
|
+
ctx;
|
|
327
|
+
options;
|
|
328
|
+
subscriptions = [];
|
|
329
|
+
constructor(ctx, options) {
|
|
330
|
+
this.ctx = ctx;
|
|
331
|
+
this.options = options;
|
|
332
|
+
}
|
|
333
|
+
/** Attach listeners; returns a disposer. Call from within an effect. */
|
|
334
|
+
start() {
|
|
335
|
+
const onEvent = (session, event) => {
|
|
336
|
+
if (event.type !== "assistant/message") return;
|
|
337
|
+
const conv = this.convIdFor(session.id);
|
|
338
|
+
if (conv === void 0) return;
|
|
339
|
+
this.relayAssistant(conv, event);
|
|
340
|
+
};
|
|
341
|
+
this.subscriptions.push(this.ctx.on("session/event", onEvent));
|
|
342
|
+
return () => {
|
|
343
|
+
for (const dispose of this.subscriptions.splice(0)) dispose();
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/** Accept one verified inbound tmim event (message.created / mention.created). */
|
|
347
|
+
async ingestMessage(args) {
|
|
348
|
+
const convId = args.convId;
|
|
349
|
+
const sessionIdStr = this.sessionIdFor(convId);
|
|
350
|
+
let agent = this.ctx.agents.get(sessionIdStr);
|
|
351
|
+
if (!agent) agent = (await this.ctx.agents.create({
|
|
352
|
+
sessionId: sessionIdStr,
|
|
353
|
+
meta: { cwd: this.options.cwd ?? process.cwd() },
|
|
354
|
+
agentOptions: this.options.agentOptions
|
|
355
|
+
})).agent;
|
|
356
|
+
const message = buildUserMessage(args.text ?? "");
|
|
357
|
+
agent.followup(message);
|
|
358
|
+
return { sessionId: sessionIdStr };
|
|
359
|
+
}
|
|
360
|
+
sessionIdFor(convId) {
|
|
361
|
+
return `tmim:${convId}`;
|
|
362
|
+
}
|
|
363
|
+
convIdFor(sessionId) {
|
|
364
|
+
const s = String(sessionId);
|
|
365
|
+
if (!s.startsWith("tmim:")) return void 0;
|
|
366
|
+
const n = Number(s.slice(5));
|
|
367
|
+
return Number.isFinite(n) ? n : void 0;
|
|
368
|
+
}
|
|
369
|
+
async relayAssistant(convId, event) {
|
|
370
|
+
const text = textOf(event.data.message);
|
|
371
|
+
if (!text) return;
|
|
372
|
+
const client = this.options.getClient();
|
|
373
|
+
try {
|
|
374
|
+
await client.sendText(convId, text, newClientMsgId$1());
|
|
375
|
+
this.ctx.logger.debug(`[tmim-bridge] relayed reply to conv ${convId}`);
|
|
376
|
+
} catch (error) {
|
|
377
|
+
this.ctx.logger.error(`[tmim-bridge] failed to relay to conv ${convId}: ${String(error)}`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
function textOf(message) {
|
|
382
|
+
if (!message) return void 0;
|
|
383
|
+
return message.content?.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text).join("") || void 0;
|
|
384
|
+
}
|
|
385
|
+
function buildUserMessage(text) {
|
|
386
|
+
return createUserMessage({
|
|
387
|
+
content: [{
|
|
388
|
+
type: "text",
|
|
389
|
+
text
|
|
390
|
+
}],
|
|
391
|
+
source: { kind: "user" }
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
function newClientMsgId$1() {
|
|
395
|
+
return `c-${randomUUID().replaceAll("-", "")}`;
|
|
396
|
+
}
|
|
397
|
+
// src/tools.ts
|
|
398
|
+
function newClientMsgId() {
|
|
399
|
+
return `c-${crypto.randomUUID().replaceAll("-", "")}`;
|
|
400
|
+
}
|
|
401
|
+
/** Render a TM IM tool's canonical JSON result as a text card for the agent history. */
|
|
402
|
+
function renderToolResult(_args, value) {
|
|
403
|
+
return [{
|
|
404
|
+
type: "text",
|
|
405
|
+
text: JSON.stringify(value, null, 2)
|
|
406
|
+
}];
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* Register TM IM tools in the current context.
|
|
410
|
+
* @returns disposer removing all registered tools.
|
|
411
|
+
*/
|
|
412
|
+
function registerTmimTools(ctx, client) {
|
|
413
|
+
return ctx.effect(function* () {
|
|
414
|
+
yield ctx.tools.register(defineTool({
|
|
415
|
+
name: "tmim_send_text",
|
|
416
|
+
description: "Send a text message to a TM IM conversation. Provide convId (numeric id) and text. Use tmim_find_conversation first if you only know the peer handle.",
|
|
417
|
+
parameters: {
|
|
418
|
+
convId: {
|
|
419
|
+
type: "number",
|
|
420
|
+
required: true,
|
|
421
|
+
description: "TM IM conversation id."
|
|
422
|
+
},
|
|
423
|
+
text: {
|
|
424
|
+
type: "string",
|
|
425
|
+
required: true,
|
|
426
|
+
description: "Text payload (Markdown allowed)."
|
|
427
|
+
},
|
|
428
|
+
replyTo: {
|
|
429
|
+
type: "number",
|
|
430
|
+
description: "Optional message_id to quote/reply to."
|
|
431
|
+
}
|
|
432
|
+
},
|
|
433
|
+
output: {
|
|
434
|
+
schema: { type: "json" },
|
|
435
|
+
render: renderToolResult
|
|
436
|
+
},
|
|
437
|
+
execute: async (args) => {
|
|
438
|
+
const result = await client().sendText(args.convId, args.text, newClientMsgId(), args.replyTo);
|
|
439
|
+
return {
|
|
440
|
+
sent: true,
|
|
441
|
+
messageId: result.messageId,
|
|
442
|
+
seq: result.seq
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
}));
|
|
446
|
+
yield ctx.tools.register(defineTool({
|
|
447
|
+
name: "tmim_get_or_create_direct_conversation",
|
|
448
|
+
description: "Get or create a direct (1:1) conversation with a peer handle (e.g. \"@alice\"). Returns the convId.",
|
|
449
|
+
parameters: { peer: {
|
|
450
|
+
type: "string",
|
|
451
|
+
required: true,
|
|
452
|
+
description: "Peer handle, e.g. \"@alice\"."
|
|
453
|
+
} },
|
|
454
|
+
output: {
|
|
455
|
+
schema: { type: "json" },
|
|
456
|
+
render: renderToolResult
|
|
457
|
+
},
|
|
458
|
+
execute: async (args) => {
|
|
459
|
+
const result = await client().getOrCreateDirectConversation(args.peer);
|
|
460
|
+
return {
|
|
461
|
+
convId: result.convId,
|
|
462
|
+
created: result.created,
|
|
463
|
+
peer: result.peer,
|
|
464
|
+
lastSeq: result.lastSeq
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}));
|
|
468
|
+
yield ctx.tools.register(defineTool({
|
|
469
|
+
name: "tmim_create_group_conversation",
|
|
470
|
+
description: "Create a group conversation with the given title and member handles.",
|
|
471
|
+
parameters: {
|
|
472
|
+
title: {
|
|
473
|
+
type: "string",
|
|
474
|
+
required: true,
|
|
475
|
+
description: "Group title."
|
|
476
|
+
},
|
|
477
|
+
members: {
|
|
478
|
+
type: "array",
|
|
479
|
+
required: true,
|
|
480
|
+
items: { type: "string" },
|
|
481
|
+
description: "Member handles (excluding yourself), e.g. [\"@alice\",\"@bob\"]."
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
output: {
|
|
485
|
+
schema: { type: "json" },
|
|
486
|
+
render: renderToolResult
|
|
487
|
+
},
|
|
488
|
+
execute: async (args) => {
|
|
489
|
+
return { convId: (await client().createGroupConversation(args.title, args.members)).convId };
|
|
490
|
+
}
|
|
491
|
+
}));
|
|
492
|
+
yield ctx.tools.register(defineTool({
|
|
493
|
+
name: "tmim_list_conversations",
|
|
494
|
+
description: "List the agent's TM IM conversations (recent first).",
|
|
495
|
+
parameters: { limit: {
|
|
496
|
+
type: "number",
|
|
497
|
+
description: "Max items (default 50)."
|
|
498
|
+
} },
|
|
499
|
+
output: {
|
|
500
|
+
schema: { type: "json" },
|
|
501
|
+
render: renderToolResult
|
|
502
|
+
},
|
|
503
|
+
execute: async (args) => {
|
|
504
|
+
const result = await client().listConversations(args.limit ?? 50);
|
|
505
|
+
return {
|
|
506
|
+
items: result.items,
|
|
507
|
+
nextCursor: result.nextCursor
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
}));
|
|
511
|
+
yield ctx.tools.register(defineTool({
|
|
512
|
+
name: "tmim_pull_messages",
|
|
513
|
+
description: "Incrementally pull new messages since a seq for a conversation (ascending).",
|
|
514
|
+
parameters: {
|
|
515
|
+
convId: {
|
|
516
|
+
type: "number",
|
|
517
|
+
required: true,
|
|
518
|
+
description: "Conversation id."
|
|
519
|
+
},
|
|
520
|
+
sinceSeq: {
|
|
521
|
+
type: "number",
|
|
522
|
+
required: true,
|
|
523
|
+
description: "Only messages with seq > this are returned."
|
|
524
|
+
},
|
|
525
|
+
limit: {
|
|
526
|
+
type: "number",
|
|
527
|
+
description: "Max items (default 200)."
|
|
528
|
+
}
|
|
529
|
+
},
|
|
530
|
+
output: {
|
|
531
|
+
schema: { type: "json" },
|
|
532
|
+
render: renderToolResult
|
|
533
|
+
},
|
|
534
|
+
execute: async (args) => {
|
|
535
|
+
return { items: await client().pullMessagesSince(args.convId, args.sinceSeq, args.limit ?? 200) };
|
|
536
|
+
}
|
|
537
|
+
}));
|
|
538
|
+
}, "tmim.tools");
|
|
539
|
+
}
|
|
540
|
+
// src/index.ts
|
|
541
|
+
const Config = z.object({
|
|
542
|
+
baseUrl: z.string().required(),
|
|
543
|
+
credential: z.union([z.object({
|
|
544
|
+
kind: z.const("apikey"),
|
|
545
|
+
token: z.string().required()
|
|
546
|
+
}), z.object({
|
|
547
|
+
kind: z.const("password"),
|
|
548
|
+
username: z.string().required(),
|
|
549
|
+
password: z.string().required()
|
|
550
|
+
})]),
|
|
551
|
+
deviceId: z.string().default(`dsh-${process.pid}`),
|
|
552
|
+
webhook: z.object({
|
|
553
|
+
enabled: z.boolean().default(false),
|
|
554
|
+
path: z.string().default("/tm/callback"),
|
|
555
|
+
secret: z.string().default(""),
|
|
556
|
+
maxSkewSeconds: z.number().default(300)
|
|
557
|
+
}),
|
|
558
|
+
enableAgentTakeover: z.boolean().default(false),
|
|
559
|
+
agent: z.object({
|
|
560
|
+
provider: z.string().default("deepseek-official"),
|
|
561
|
+
model: z.string().required()
|
|
562
|
+
}),
|
|
563
|
+
cwd: z.string().default(process.cwd()),
|
|
564
|
+
tools: z.boolean().default(true)
|
|
565
|
+
});
|
|
566
|
+
const name = "tmim";
|
|
567
|
+
async function apply(ctx, config) {
|
|
568
|
+
if (config.webhook.enabled && !config.webhook.secret) throw new Error("[tmim] webhook.enabled requires webhook.secret (the TM IM webhook_secret)");
|
|
569
|
+
const client = new TmimClient({
|
|
570
|
+
baseUrl: config.baseUrl,
|
|
571
|
+
...config.credential.kind === "apikey" ? { token: config.credential.token } : {},
|
|
572
|
+
deviceId: config.deviceId
|
|
573
|
+
});
|
|
574
|
+
if (config.credential.kind === "password") {
|
|
575
|
+
await client.login(config.credential.username, config.credential.password);
|
|
576
|
+
ctx.logger.info("[tmim] authenticated via password login");
|
|
577
|
+
} else {
|
|
578
|
+
client.setToken(config.credential.token);
|
|
579
|
+
ctx.logger.info("[tmim] using Agent api key");
|
|
580
|
+
}
|
|
581
|
+
if (config.enableAgentTakeover) {
|
|
582
|
+
if ((await client.configureAgentTakeover({
|
|
583
|
+
...config.webhook.enabled ? {
|
|
584
|
+
pushMode: 1,
|
|
585
|
+
endpointUrl: config.webhook.path
|
|
586
|
+
} : { pushMode: 3 },
|
|
587
|
+
capabilities: ["text"],
|
|
588
|
+
modelInfo: config.agent.model
|
|
589
|
+
})).webhookSecret && config.webhook.enabled) ctx.logger.warn("[tmim] NEW webhook_secret issued (shown only once) — save it and set webhook.secret. Rotate anytime via POST /v1/me/agent-takeover/rotate-webhook-secret");
|
|
590
|
+
}
|
|
591
|
+
const bridge = new TmimBridge(ctx, {
|
|
592
|
+
getClient: () => client,
|
|
593
|
+
agentOptions: {
|
|
594
|
+
provider: config.agent.provider,
|
|
595
|
+
model: config.agent.model
|
|
596
|
+
},
|
|
597
|
+
cwd: config.cwd
|
|
598
|
+
});
|
|
599
|
+
ctx.effect(function* () {
|
|
600
|
+
if (config.webhook.enabled) yield mountTmimWebhook(ctx, {
|
|
601
|
+
path: config.webhook.path,
|
|
602
|
+
secret: config.webhook.secret,
|
|
603
|
+
maxSkewSeconds: config.webhook.maxSkewSeconds,
|
|
604
|
+
handle: (event) => handleWebhookEvent(bridge, event)
|
|
605
|
+
});
|
|
606
|
+
if (config.tools) yield registerTmimTools(ctx, () => client);
|
|
607
|
+
yield bridge.start();
|
|
608
|
+
ctx.logger.info("[tmim] started (webhook=%s, tools=%s)", config.webhook.enabled, config.tools);
|
|
609
|
+
}, "tmim");
|
|
610
|
+
}
|
|
611
|
+
function handleWebhookEvent(bridge, event) {
|
|
612
|
+
if (event.event !== "message.created" && event.event !== "mention.created") return;
|
|
613
|
+
const data = event.data;
|
|
614
|
+
if (data.sender?.actor_type === 2) return;
|
|
615
|
+
const replyTo = Number(data.reply_to ?? 0) || void 0;
|
|
616
|
+
bridge.ingestMessage({
|
|
617
|
+
convId: Number(data.conv_id),
|
|
618
|
+
senderId: Number(data.sender_id),
|
|
619
|
+
text: data.content?.text,
|
|
620
|
+
...replyTo === void 0 ? {} : { replyToMessageId: replyTo }
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
export { Config, apply, name };
|
|
624
|
+
|
|
625
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["newClientMsgId"],"sources":["../src/tmim-client.ts","../src/crypto.ts","../src/webhook.ts","../src/bridge.ts","../src/tools.ts","../src/index.ts"],"sourcesContent":["/**\r\n * Minimal TM IM REST client (user-end `/v1`) for a Harness agent.\r\n *\r\n * Implements the endpoints a connector needs:\r\n * - auth (login / refresh / agent takeover api key)\r\n * - direct conversation get-or-create\r\n * - group conversation create\r\n * - list conversations\r\n * - send a text message (with idempotent client_msg_id)\r\n * - incremental pull (since_seq) and cursor pull\r\n * - agent takeover configuration (auth + status + enable/disable WEBHOOK)\r\n *\r\n * All requests expect the TM IM unified envelope:\r\n * { \"code\": 0, \"message\": \"ok\", \"data\": {...} }\r\n * where a non-zero `code` is a business error returned over HTTP 200.\r\n */\r\n\r\nexport interface TmimConfig {\r\n /** Base URL, e.g. `https://tmim.example.com`. Do NOT append `/v1`. */\r\n baseUrl: string\r\n /** Authentication: either an Agent takeover api_key (`Bearer`) ... */\r\n token?: string\r\n /** ... or credentials used to log in and obtain a JWT. */\r\n username?: string\r\n password?: string\r\n /** Stable device id reported via `X-TM-Device-Id`. */\r\n deviceId?: string\r\n /** Custom fetch; defaults to global fetch. */\r\n fetch?: typeof fetch\r\n /** Timeout for each HTTP request, ms. */\r\n timeoutMs?: number\r\n}\r\n\r\ninterface TmimEnvelope<T> {\r\n code: number\r\n message: string\r\n data: T | null\r\n}\r\n\r\nexport class TmimError extends Error {\r\n constructor(\r\n /** Business code from the envelope. 0 means transport-level success. */\r\n public readonly code: number,\r\n message: string,\r\n public readonly httpStatus?: number,\r\n public readonly payload?: unknown,\r\n ) {\r\n super(message)\r\n this.name = 'TmimError'\r\n }\r\n}\r\n\r\nexport class TmimClient {\r\n private token: string | undefined\r\n private fetchImpl: typeof fetch\r\n private timeoutMs: number\r\n\r\n constructor(private readonly config: TmimConfig) {\r\n this.token = config.token\r\n this.fetchImpl = config.fetch ?? globalThis.fetch.bind(globalThis)\r\n this.timeoutMs = config.timeoutMs ?? 15_000\r\n }\r\n\r\n /** Current bearer token (may be a JWT or an Agent api_key). */\r\n get authToken(): string | undefined {\r\n return this.token\r\n }\r\n\r\n // ---- low level ----\r\n\r\n private async request<T>(\r\n method: string,\r\n path: string,\r\n body?: unknown,\r\n opts: { auth?: boolean; headers?: Record<string, string> } = {},\r\n ): Promise<T> {\r\n const url = `${this.config.baseUrl}/v1${path}`\r\n const headers: Record<string, string> = {\r\n Accept: 'application/json',\r\n ...(opts.headers ?? {}),\r\n }\r\n if (body !== undefined) headers['Content-Type'] = 'application/json; charset=utf-8'\r\n if (this.config.deviceId) headers['X-TM-Device-Id'] = this.config.deviceId\r\n if (opts.auth !== false && this.token) headers['Authorization'] = `Bearer ${this.token}`\r\n\r\n const controller = new AbortController()\r\n const timer = setTimeout(() => controller.abort(), this.timeoutMs)\r\n try {\r\n const res = await this.fetchImpl(url, {\r\n method,\r\n headers,\r\n ...(body === undefined ? {} : { body: JSON.stringify(body) }),\r\n signal: controller.signal,\r\n })\r\n let envelope: TmimEnvelope<T> | undefined\r\n let text = ''\r\n try {\r\n text = await res.text()\r\n envelope = JSON.parse(text) as TmimEnvelope<T>\r\n } catch {\r\n envelope = undefined\r\n }\r\n\r\n if (res.status === 401 || res.status === 403) {\r\n throw new TmimError(res.status, `tmim auth failed (HTTP ${res.status})`, res.status, text)\r\n }\r\n if (!res.ok && !envelope) {\r\n throw new TmimError(0, `tmim HTTP ${res.status}: ${text.slice(0, 300)}`, res.status, text)\r\n }\r\n if (envelope && envelope.code !== 0) {\r\n throw new TmimError(envelope.code, envelope.message ?? `tmim error ${envelope.code}`, res.status, envelope)\r\n }\r\n return envelope?.data ?? (undefined as T)\r\n } finally {\r\n clearTimeout(timer)\r\n }\r\n }\r\n\r\n // ---- auth ----\r\n\r\n /** Login with handle/password; stores the returned JWT. */\r\n async login(handle: string, password: string): Promise<{ token: string; refreshToken: string }> {\r\n const data = await this.request<{\r\n token: string\r\n refresh_token: string\r\n actor: unknown\r\n }>('POST', '/auth/login', { handle, password }, { auth: false })\r\n this.token = data.token\r\n return { token: data.token, refreshToken: data.refresh_token }\r\n }\r\n\r\n /** Exchange a refresh token for a new access token (refresh_token is one-time). */\r\n async refresh(refreshToken: string): Promise<{ token: string; refreshToken: string }> {\r\n const data = await this.request<{ token: string; refresh_token: string }>(\r\n 'POST',\r\n '/auth/refresh',\r\n { refresh_token: refreshToken },\r\n { auth: false },\r\n )\r\n this.token = data.token\r\n return { token: data.token, refreshToken: data.refresh_token }\r\n }\r\n\r\n /** Set an explicit bearer token (e.g. an Agent takeover api_key). */\r\n setToken(token: string): this {\r\n this.token = token\r\n return this\r\n }\r\n\r\n // ---- agent takeover ----\r\n\r\n /**\r\n * Configure Agent Takeover.\r\n * doc: POST /v1/me/agent-takeover\r\n * Returns the issued webhook_secret ONLY when the merged push mode is\r\n * WEBHOOK and no secret existed yet (shown exactly once).\r\n */\r\n async configureAgentTakeover(args: {\r\n pushMode: 1 | 2 | 3 // 1=WEBHOOK, 2=LONG_POLL, 3=PULL\r\n endpointUrl?: string\r\n capabilities?: string[]\r\n modelInfo?: string\r\n rateLimit?: number\r\n }): Promise<{ webhookSecret?: string }> {\r\n const body: Record<string, unknown> = { push_mode: args.pushMode }\r\n if (args.endpointUrl !== undefined) body.endpoint_url = args.endpointUrl\r\n if (args.capabilities !== undefined) body.capabilities = args.capabilities\r\n if (args.modelInfo !== undefined) body.model_info = args.modelInfo\r\n if (args.rateLimit !== undefined) body.rate_limit = args.rateLimit\r\n return this.request<{ webhookSecret?: string }>('POST', '/me/agent-takeover', body)\r\n }\r\n\r\n /** Rotate the webhook secret; returns the new plaintext exactly once. */\r\n async rotateWebhookSecret(): Promise<{ webhookSecret: string }> {\r\n return this.request<{ webhookSecret: string }>(\r\n 'POST',\r\n '/me/agent-takeover/rotate-webhook-secret',\r\n )\r\n }\r\n\r\n /** Disable Agent Takeover entirely. */\r\n async disableAgentTakeover(): Promise<void> {\r\n await this.request<null>('DELETE', '/me/agent-takeover')\r\n }\r\n\r\n /** Read current takeover config (never includes secrets). */\r\n async getAgentTakeoverStatus(): Promise<{\r\n actorId: number\r\n enabled: boolean\r\n pushMode: number\r\n endpointUrl?: string\r\n hasApiKey: boolean\r\n hasWebhookSecret: boolean\r\n }> {\r\n const d = await this.request<{\r\n actor_id: number\r\n enabled: boolean\r\n push_mode: number\r\n endpoint_url?: string\r\n has_api_key: boolean\r\n has_webhook_secret: boolean\r\n }>('GET', '/me/agent-takeover')\r\n return {\r\n actorId: d.actor_id,\r\n enabled: d.enabled,\r\n pushMode: d.push_mode,\r\n ...(d.endpoint_url === undefined ? {} : { endpointUrl: d.endpoint_url }),\r\n hasApiKey: d.has_api_key,\r\n hasWebhookSecret: d.has_webhook_secret,\r\n }\r\n }\r\n\r\n // ---- conversations ----\r\n\r\n /** Get or create a direct (1:1) conversation with a peer. */\r\n async getOrCreateDirectConversation(peer: string): Promise<{\r\n convId: number\r\n convType: number\r\n peer: { actor_id: number; handle: string; display_name: string; online: boolean }\r\n created: boolean\r\n lastSeq: number\r\n }> {\r\n const d = await this.request<{\r\n conv_id: number\r\n conv_type: number\r\n peer: { actor_id: number; handle: string; display_name: string; online: boolean }\r\n created: boolean\r\n last_seq: number\r\n }>('POST', '/conversations/direct', { peer })\r\n return {\r\n convId: d.conv_id,\r\n convType: d.conv_type,\r\n peer: d.peer,\r\n created: d.created,\r\n lastSeq: d.last_seq,\r\n }\r\n }\r\n\r\n /** Create a group conversation. `members` excludes the caller. */\r\n async createGroupConversation(title: string, members: string[]): Promise<{ convId: number }> {\r\n const d = await this.request<{ conv_id: number }>('POST', '/conversations/group', { title, members })\r\n return { convId: d.conv_id }\r\n }\r\n\r\n /** List the caller's conversations (cursor paginated). */\r\n async listConversations(limit = 50, cursor?: string): Promise<{\r\n items: Array<{ conv_id: number; conv_type: number; title: string | null; last_seq: number }>\r\n nextCursor: string | null\r\n }> {\r\n const q = new URLSearchParams({ limit: String(limit) })\r\n if (cursor) q.set('cursor', cursor)\r\n const d = await this.request<{ items: Array<{ conv_id: number } & Record<string, unknown>>; next_cursor: string | null }>('GET', `/conversations?${q}`)\r\n return { items: d.items as any, nextCursor: d.next_cursor }\r\n }\r\n\r\n // ---- messages ----\r\n\r\n /**\r\n * Send a text message. `clientMsgId` is the idempotency key; re-sending the\r\n * same key returns the identical original message (no duplicate).\r\n */\r\n async sendText(convId: number, text: string, clientMsgId: string, replyTo?: number): Promise<{\r\n messageId: number\r\n convId: number\r\n seq: number\r\n clientMsgId: string\r\n senderId: number\r\n createdAt: string\r\n }> {\r\n const d = await this.request<{\r\n message_id: number\r\n conv_id: number\r\n seq: number\r\n client_msg_id: string\r\n sender_id: number\r\n created_at: string\r\n }>('POST', `/conversations/${convId}/messages`, {\r\n msg_type: 'TEXT',\r\n client_msg_id: clientMsgId,\r\n content: { text },\r\n ...(replyTo === undefined ? {} : { reply_to: replyTo }),\r\n })\r\n return {\r\n messageId: d.message_id,\r\n convId: d.conv_id,\r\n seq: d.seq,\r\n clientMsgId: d.client_msg_id,\r\n senderId: d.sender_id,\r\n createdAt: d.created_at,\r\n }\r\n }\r\n\r\n /** Incremental pull: messages with seq > sinceSeq, ascending. */\r\n async pullMessagesSince(convId: number, sinceSeq: number, limit = 200): Promise<Array<{\r\n message_id: number\r\n conv_id: number\r\n seq: number\r\n sender_id: number\r\n msg_type: string\r\n content: { text?: string }\r\n created_at: string\r\n }>> {\r\n const q = new URLSearchParams({ since_seq: String(sinceSeq), limit: String(limit) })\r\n const d = await this.request<{ items: Array<any> }>('GET', `/conversations/${convId}/messages?${q}`)\r\n return d.items ?? []\r\n }\r\n\r\n /** Cursor pull: newest-first history page. */\r\n async pullConversationHistory(convId: number, cursor?: string, limit = 50): Promise<{\r\n items: Array<Record<string, unknown>>\r\n nextCursor: string | null\r\n hasMore: boolean\r\n }> {\r\n const q = new URLSearchParams({ limit: String(limit) })\r\n if (cursor) q.set('cursor', cursor)\r\n const d = await this.request<{\r\n items: Array<Record<string, unknown>>\r\n next_cursor: string | null\r\n has_more: boolean\r\n }>('GET', `/conversations/${convId}/messages?${q}`)\r\n return { items: d.items ?? [], nextCursor: d.next_cursor, hasMore: d.has_more }\r\n }\r\n}\r\n","/**\r\n * Webhook signature verification per the TM IM protocol.\r\n *\r\n * Signing string = `${X-TM-Timestamp}.${rawRequestBody}`\r\n * Signature = HMAC-SHA256(webhook_secret, signing_string), hex-encoded, lowercase\r\n * Header = `X-TM-Signature: sha256=<hex>`\r\n *\r\n * The raw request body bytes MUST be used; re-serializing the parsed JSON\r\n * changes the byte layout and breaks the signature.\r\n */\r\n\r\nimport { createHmac, timingSafeEqual } from 'node:crypto'\r\n\r\n/** Hard reference-point used only by the documented self-test vector. */\r\nexport const SIGNATURE_HEADER_PREFIX = 'sha256='\r\n\r\nexport interface WebhookVerificationInput {\r\n /** Value of the `webhook_secret` issued by TM IM when WEBHOOK push mode was enabled. */\r\n secret: string\r\n /** String value of the `X-TM-Timestamp` header (Unix seconds). */\r\n timestamp: string\r\n /** Raw request body bytes, exactly as received on the wire. */\r\n rawBody: Buffer\r\n /** Value of the `X-TM-Signature` header (e.g. `sha256=abc...`). */\r\n signatureHeader: string | undefined\r\n /** Optional current time in whole seconds; defaults to `Math.floor(Date.now()/1000)`. */\r\n now?: number\r\n /** Maximum acceptable clock skew in seconds (protocol default 300). */\r\n maxSkewSeconds?: number\r\n}\r\n\r\nexport type VerificationResult =\r\n | { ok: true }\r\n | { ok: false; reason: 'missing-secret' }\r\n | { ok: false; reason: 'missing-header' }\r\n | { ok: false; reason: 'bad-format' }\r\n | { ok: false; reason: 'replay' }\r\n | { ok: false; reason: 'mismatch' }\r\n\r\n/** Constant-time HMAC-SHA256 verification helper. */\r\nexport function hmacSha256(secret: string, message: string): string {\r\n return createHmac('sha256', secret).update(message, 'utf8').digest('hex')\r\n}\r\n\r\n/** Verify a webhook request signature and freshness. Exported for reuse and tests. */\r\nexport function verifyWebhookSignature(input: WebhookVerificationInput): VerificationResult {\r\n const { secret, timestamp, rawBody, signatureHeader } = input\r\n if (!secret) return { ok: false, reason: 'missing-secret' }\r\n if (!signatureHeader) return { ok: false, reason: 'missing-header' }\r\n if (!signatureHeader.startsWith(SIGNATURE_HEADER_PREFIX)) {\r\n return { ok: false, reason: 'bad-format' }\r\n }\r\n const provided = signatureHeader.slice(SIGNATURE_HEADER_PREFIX.length)\r\n if (!/^[0-9a-f]{64}$/i.test(provided)) return { ok: false, reason: 'bad-format' }\r\n\r\n // Replay protection: reject timestamps too far from now.\r\n const ts = Number(timestamp)\r\n if (!Number.isFinite(ts)) return { ok: false, reason: 'bad-format' }\r\n const now = input.now ?? Math.floor(Date.now() / 1000)\r\n const maxSkew = input.maxSkewSeconds ?? 300\r\n if (Math.abs(ts - now) > maxSkew) return { ok: false, reason: 'replay' }\r\n\r\n const expected = hmacSha256(secret, `${timestamp}.${rawBody.toString('utf8')}`)\r\n const a = Buffer.from(provided.toLowerCase(), 'utf8')\r\n const b = Buffer.from(expected, 'utf8')\r\n const match = a.length === b.length && timingSafeEqual(a, b)\r\n return match ? { ok: true } : { ok: false, reason: 'mismatch' }\r\n}\r\n\r\n/** Parse the `X-TM-Signature` header into its raw hex digest, if well-formed. */\r\nexport function parseSignature(signatureHeader: string): string | undefined {\r\n if (!signatureHeader.startsWith(SIGNATURE_HEADER_PREFIX)) return undefined\r\n const hex = signatureHeader.slice(SIGNATURE_HEADER_PREFIX.length)\r\n return /^[0-9a-f]{64}$/i.test(hex) ? hex.toLowerCase() : undefined\r\n}\r\n","/**\r\n * TM IM webhook receiver.\r\n *\r\n * Mounts an exact HTTP route on the Harness web server that TM IM POSTs\r\n * agent-takeover events to. It:\r\n * 1. Verifies `X-TM-Signature` (HMAC-SHA256 over `ts.body`) and freshness.\r\n * 2. Deduplicates by `X-TM-Event-Id`.\r\n * 3. Returns 200 fast, then hands the event to a handler (async processing).\r\n *\r\n * Route handler owns the full response lifecycle (Harness WebServer contract).\r\n */\r\n\r\nimport type { IncomingMessage, ServerResponse } from 'node:http'\r\nimport type { Context } from '@deepseek-ai/cordis'\r\nimport type { WebRoute } from '@deepseek-ai/dsh-host-webserver'\r\nimport { verifyWebhookSignature, type VerificationResult } from './crypto.ts'\r\n\r\nexport interface WebhookEvent {\r\n event: string\r\n event_id: string\r\n occurred_at: number\r\n data: Record<string, unknown>\r\n}\r\n\r\nexport interface TmimWebhookOptions {\r\n /** Path this receiver is mounted at. Must match the endpoint_url configured in TM IM. */\r\n path: string\r\n /** webhook_secret issued by TM IM. */\r\n secret: string\r\n /** Called with each verified, non-duplicate event. Return once handed off. */\r\n handle: (event: WebhookEvent, meta: { deliveryAttempt: number }) => void | Promise<void>\r\n /** In-memory dedup set. Replace to persist across restarts. */\r\n processedEventIds?: Set<string>\r\n /** Clock skew tolerance, seconds (protocol default 300). */\r\n maxSkewSeconds?: number\r\n}\r\n\r\nfunction readBody(req: IncomingMessage): Promise<Buffer> {\r\n return new Promise((resolve, reject) => {\r\n const chunks: Buffer[] = []\r\n req.on('data', (c: Buffer) => chunks.push(c))\r\n req.on('end', () => resolve(Buffer.concat(chunks)))\r\n req.on('error', reject)\r\n })\r\n}\r\n\r\n/**\r\n * Register the webhook receiver on a Harness `webServer` context.\r\n * @returns a disposer that unmounts the route.\r\n */\r\nexport function mountTmimWebhook(ctx: Context, options: TmimWebhookOptions): () => void {\r\n const webServer = ctx.webServer\r\n const processed = options.processedEventIds ?? new Set<string>()\r\n const seen = new Map<string, string>() // event_id -> last delivery attempt\r\n\r\n const handler = async (req: IncomingMessage, res: ServerResponse): Promise<void> => {\r\n if (req.method !== 'POST') {\r\n res.writeHead(405, { 'Content-Type': 'application/json' })\r\n res.end(JSON.stringify({ error: 'method not allowed' }))\r\n return\r\n }\r\n\r\n // Capture the RAW body bytes BEFORE any parsing (signature correctness).\r\n const raw = await readBody(req)\r\n const header = (name: string) => req.headers[name.toLowerCase()]?.toString()\r\n\r\n const tsHeader = header('x-tm-timestamp')\r\n const sigHeader = header('x-tm-signature')\r\n const eventId = header('x-tm-event-id')\r\n const deliveryAttempt = Number(header('x-tm-delivery-attempt') ?? '1')\r\n\r\n const result = verifyWebhookSignature({\r\n secret: options.secret,\r\n timestamp: tsHeader ?? '',\r\n rawBody: raw,\r\n signatureHeader: sigHeader,\r\n ...(options.maxSkewSeconds === undefined ? {} : { maxSkewSeconds: options.maxSkewSeconds }),\r\n })\r\n\r\n if (!result.ok || !tsHeader) {\r\n respond(res, 400, 'unauthenticated')\r\n ctx.logger.warn(`[tmim-webhook] rejected event ${eventId ?? '(no id)'}: ${deserializeResult(result)}`)\r\n return\r\n }\r\n\r\n // Idempotency: same event_id and same-or-higher attempt -> already handled.\r\n const last = seen.get(eventId ?? '')\r\n if (eventId && last !== undefined && Number(last) <= deliveryAttempt) {\r\n respond(res, 200, 'duplicate')\r\n return\r\n }\r\n\r\n let event: WebhookEvent\r\n try {\r\n event = JSON.parse(raw.toString('utf8')) as WebhookEvent\r\n } catch {\r\n respond(res, 400, 'bad json')\r\n return\r\n }\r\n\r\n // Record before handing off; rapid retries must see \"already handled\".\r\n if (eventId) seen.set(eventId, String(deliveryAttempt))\r\n if (eventId) processed.add(eventId)\r\n\r\n respond(res, 200, 'ok')\r\n\r\n // Async processing after the fast 200 (5s limit is on HTTP return, not work).\r\n try {\r\n await options.handle(event, { deliveryAttempt })\r\n } catch (error) {\r\n ctx.logger.error(`[tmim-webhook] handler failed for event ${eventId}: ${String(error)}`)\r\n }\r\n }\r\n\r\n const dispose = webServer.register({\r\n kind: 'exact',\r\n path: options.path,\r\n handler,\r\n } satisfies WebRoute)\r\n\r\n ctx.logger.info(`[tmim-webhook] mounted at ${options.path}`)\r\n return dispose\r\n}\r\n\r\nfunction deserializeResult(result: VerificationResult): string {\r\n return result.ok ? 'ok' : result.reason\r\n}\r\n\r\nfunction respond(res: ServerResponse, status: number, statusText: string): void {\r\n res.writeHead(status, { 'Content-Type': 'application/json' })\r\n res.end(JSON.stringify({ status: statusText }))\r\n}\r\n","/**\r\n * Bridge: turn inbound TM IM events into Harness agent turns, and relay the\r\n * agent's assistant replies back to the TM IM conversation.\r\n *\r\n * One Harness session is maintained per TM IM conversation (`tmim:<convId>`).\r\n * Inbound messages become `user` messages via `agent.followup(createUserMessage(...))`.\r\n * Assistant output is observed through `session/event` (`assistant/message`) and\r\n * sent back through `TmimClient.sendText`.\r\n */\r\n\r\nimport { randomUUID } from 'node:crypto'\r\nimport type { Context } from '@deepseek-ai/cordis'\r\nimport { createUserMessage, type UserMessage } from '@deepseek-ai/dsh-llm'\r\nimport type { SessionId } from '@deepseek-ai/dsh-session'\r\nimport type { TmimClient } from './tmim-client.ts'\r\n\r\nexport interface BridgeOptions {\r\n /** Provide the authenticated TM IM client. */\r\n getClient: () => TmimClient\r\n /** Provider/model selection passed to `ctx.agents.create`. */\r\n agentOptions: { provider: string; model: string }\r\n /** Working directory for newly-created agents. */\r\n cwd?: string\r\n}\r\n\r\nexport class TmimBridge {\r\n private subscriptions: Array<() => void> = []\r\n\r\n constructor(private ctx: Context, private options: BridgeOptions) {}\r\n\r\n /** Attach listeners; returns a disposer. Call from within an effect. */\r\n start(): () => void {\r\n const onEvent = (session: { id: SessionId }, event: { type: string; data: { message?: any } }) => {\r\n if (event.type !== 'assistant/message') return\r\n const conv = this.convIdFor(session.id)\r\n if (conv === undefined) return\r\n void this.relayAssistant(conv, event)\r\n }\r\n this.subscriptions.push(this.ctx.on('session/event' as any, onEvent as any))\r\n return () => {\r\n for (const dispose of this.subscriptions.splice(0)) dispose()\r\n }\r\n }\r\n\r\n /** Accept one verified inbound tmim event (message.created / mention.created). */\r\n async ingestMessage(args: {\r\n convId: number\r\n senderId: number\r\n text?: string\r\n replyToMessageId?: number\r\n }): Promise<{ sessionId: string }> {\r\n const convId = args.convId\r\n const sessionIdStr = this.sessionIdFor(convId)\r\n let agent = this.ctx.agents.get(sessionIdStr as SessionId)\r\n if (!agent) {\r\n const handle = await this.ctx.agents.create({\r\n sessionId: sessionIdStr as SessionId,\r\n meta: { cwd: this.options.cwd ?? process.cwd() },\r\n agentOptions: this.options.agentOptions,\r\n })\r\n agent = handle.agent\r\n }\r\n const message = buildUserMessage(args.text ?? '')\r\n agent.followup(message)\r\n return { sessionId: sessionIdStr }\r\n }\r\n\r\n // ---- internals ----\r\n\r\n private sessionIdFor(convId: number): string {\r\n return `tmim:${convId}`\r\n }\r\n\r\n private convIdFor(sessionId: SessionId): number | undefined {\r\n const s = String(sessionId)\r\n if (!s.startsWith('tmim:')) return undefined\r\n const n = Number(s.slice('tmim:'.length))\r\n return Number.isFinite(n) ? n : undefined\r\n }\r\n\r\n private async relayAssistant(\r\n convId: number,\r\n event: { data: { message?: { content?: Array<{ type: string; text?: string }> } } },\r\n ): Promise<void> {\r\n const text = textOf(event.data.message)\r\n if (!text) return\r\n const client = this.options.getClient()\r\n try {\r\n await client.sendText(convId, text, newClientMsgId())\r\n this.ctx.logger.debug(`[tmim-bridge] relayed reply to conv ${convId}`)\r\n } catch (error) {\r\n this.ctx.logger.error(`[tmim-bridge] failed to relay to conv ${convId}: ${String(error)}`)\r\n }\r\n }\r\n}\r\n\r\nfunction textOf(message: { content?: Array<{ type: string; text?: string }> } | undefined): string | undefined {\r\n if (!message) return undefined\r\n return message.content\r\n ?.filter((block) => block.type === 'text' && typeof block.text === 'string')\r\n .map((block) => (block as { text: string }).text)\r\n .join('') || undefined\r\n}\r\n\r\nfunction buildUserMessage(text: string): UserMessage {\r\n return createUserMessage({\r\n content: [{ type: 'text', text }],\r\n source: { kind: 'user' },\r\n })\r\n}\r\n\r\nfunction newClientMsgId(): string {\r\n return `c-${randomUUID().replaceAll('-', '')}`\r\n}\r\n","/**\r\n * Tools exposed to the Harness agent so it can operate the TM IM network\r\n * proactively (proactive sends, conversation discovery, etc.) — independent of\r\n * the inbound webhook path.\r\n */\r\n\r\nimport type { Context } from '@deepseek-ai/cordis'\r\nimport { defineTool } from '@deepseek-ai/dsh-tools'\r\nimport type { ContentBlock } from '@deepseek-ai/dsh-llm'\r\nimport type { TmimClient } from './tmim-client.ts'\r\n\r\nfunction newClientMsgId(): string {\r\n return `c-${crypto.randomUUID().replaceAll('-', '')}`\r\n}\r\n\r\n/** Render a TM IM tool's canonical JSON result as a text card for the agent history. */\r\nfunction renderToolResult(_args: unknown, value: unknown): ContentBlock[] {\r\n return [{ type: 'text', text: JSON.stringify(value, null, 2) }]\r\n}\r\n\r\n/**\r\n * Register TM IM tools in the current context.\r\n * @returns disposer removing all registered tools.\r\n */\r\nexport function registerTmimTools(ctx: Context, client: () => TmimClient): () => void | Promise<void> {\r\n const dispose = ctx.effect(function* () {\r\n yield ctx.tools.register(defineTool({\r\n name: 'tmim_send_text',\r\n description:\r\n 'Send a text message to a TM IM conversation. Provide convId (numeric id) and text. ' +\r\n 'Use tmim_find_conversation first if you only know the peer handle.',\r\n parameters: {\r\n convId: { type: 'number', required: true, description: 'TM IM conversation id.' },\r\n text: { type: 'string', required: true, description: 'Text payload (Markdown allowed).' },\r\n replyTo: { type: 'number', description: 'Optional message_id to quote/reply to.' },\r\n },\r\n output: { schema: { type: 'json' } as const, render: renderToolResult },\r\n execute: async (args) => {\r\n const result = await client().sendText(args.convId, args.text, newClientMsgId(), args.replyTo)\r\n return { sent: true, messageId: result.messageId, seq: result.seq }\r\n },\r\n }))\r\n\r\n yield ctx.tools.register(defineTool({\r\n name: 'tmim_get_or_create_direct_conversation',\r\n description:\r\n 'Get or create a direct (1:1) conversation with a peer handle (e.g. \"@alice\"). Returns the convId.',\r\n parameters: {\r\n peer: { type: 'string', required: true, description: 'Peer handle, e.g. \"@alice\".' },\r\n },\r\n output: { schema: { type: 'json' } as const, render: renderToolResult },\r\n execute: async (args) => {\r\n const result = await client().getOrCreateDirectConversation(args.peer)\r\n return {\r\n convId: result.convId,\r\n created: result.created,\r\n peer: result.peer,\r\n lastSeq: result.lastSeq,\r\n }\r\n },\r\n }))\r\n\r\n yield ctx.tools.register(defineTool({\r\n name: 'tmim_create_group_conversation',\r\n description: 'Create a group conversation with the given title and member handles.',\r\n parameters: {\r\n title: { type: 'string', required: true, description: 'Group title.' },\r\n members: {\r\n type: 'array',\r\n required: true,\r\n items: { type: 'string' },\r\n description: 'Member handles (excluding yourself), e.g. [\"@alice\",\"@bob\"].',\r\n },\r\n },\r\n output: { schema: { type: 'json' } as const, render: renderToolResult },\r\n execute: async (args) => {\r\n const result = await client().createGroupConversation(args.title, args.members)\r\n return { convId: result.convId }\r\n },\r\n }))\r\n\r\n yield ctx.tools.register(defineTool({\r\n name: 'tmim_list_conversations',\r\n description: 'List the agent\\'s TM IM conversations (recent first).',\r\n parameters: {\r\n limit: { type: 'number', description: 'Max items (default 50).' },\r\n },\r\n output: { schema: { type: 'json' } as const, render: renderToolResult },\r\n execute: async (args) => {\r\n const result = await client().listConversations(args.limit ?? 50)\r\n return { items: result.items, nextCursor: result.nextCursor }\r\n },\r\n }))\r\n\r\n yield ctx.tools.register(defineTool({\r\n name: 'tmim_pull_messages',\r\n description: 'Incrementally pull new messages since a seq for a conversation (ascending).',\r\n parameters: {\r\n convId: { type: 'number', required: true, description: 'Conversation id.' },\r\n sinceSeq: { type: 'number', required: true, description: 'Only messages with seq > this are returned.' },\r\n limit: { type: 'number', description: 'Max items (default 200).' },\r\n },\r\n output: { schema: { type: 'json' } as const, render: renderToolResult },\r\n execute: async (args) => {\r\n const items = await client().pullMessagesSince(args.convId, args.sinceSeq, args.limit ?? 200)\r\n return { items }\r\n },\r\n }))\r\n }, 'tmim.tools')\r\n return dispose\r\n}\r\n","/**\r\n * @tmim/dsh-plugin-tmim\r\n *\r\n * DeepSeek Harness plugin that connects to the TM IM network.\r\n *\r\n * Capabilities:\r\n * - REST client for the user-end `/v1` API (send, conversations, pull).\r\n * - Agent Takeover WEBHOOK receiver (signature-verified, idempotent).\r\n * - Bridges inbound TM IM messages into Harness agent turns and relays\r\n * assistant replies back.\r\n * - Optionally registers tools so the agent can drive TM IM proactively.\r\n *\r\n * Auth modes (choose one):\r\n * - `kind: 'apikey'` + `token`: an Agent takeover api_key (Bearer). Recommended.\r\n * - `kind: 'password'` + `username` + `password`: login to get a JWT.\r\n *\r\n * Push mode:\r\n * - `webhook.enabled: true`: tmim POSTs events to `${webhook.path}` on the\r\n * Harness web server. That URL must be publicly reachable and set as the\r\n * `endpoint_url` in TM IM Agent Takeover.\r\n * - disabled: the bridge only reacts to REST tool calls; no inbound auto-turn.\r\n */\r\n\r\nimport type { Context } from '@deepseek-ai/cordis'\r\nimport z from '@deepseek-ai/schemastery'\r\nimport { TmimClient } from './tmim-client.ts'\r\nimport { mountTmimWebhook, type WebhookEvent } from './webhook.ts'\r\nimport { TmimBridge } from './bridge.ts'\r\nimport { registerTmimTools } from './tools.ts'\r\n\r\n// ---- Config ----\r\n\r\nexport interface WebhookConfig {\r\n enabled: boolean\r\n path: string\r\n secret: string\r\n maxSkewSeconds: number\r\n}\r\n\r\nexport interface AgentConfig {\r\n provider: string\r\n model: string\r\n}\r\n\r\nexport type Credential =\r\n | { kind: 'apikey'; token: string }\r\n | { kind: 'password'; username: string; password: string }\r\n\r\nexport type Config = {\r\n baseUrl: string\r\n credential: Credential\r\n deviceId: string\r\n webhook: WebhookConfig\r\n enableAgentTakeover: boolean\r\n agent: AgentConfig\r\n cwd: string\r\n tools: boolean\r\n}\r\n\r\nexport const Config = (z.object({\r\n baseUrl: z.string().required(),\r\n credential: z.union([\r\n z.object({ kind: z.const('apikey'), token: z.string().required() }),\r\n z.object({\r\n kind: z.const('password'),\r\n username: z.string().required(),\r\n password: z.string().required(),\r\n }),\r\n ]),\r\n deviceId: z.string().default(`dsh-${process.pid}`),\r\n webhook: z.object({\r\n enabled: z.boolean().default(false),\r\n path: z.string().default('/tm/callback'),\r\n secret: z.string().default(''),\r\n maxSkewSeconds: z.number().default(300),\r\n }),\r\n enableAgentTakeover: z.boolean().default(false),\r\n agent: z.object({\r\n provider: z.string().default('deepseek-official'),\r\n model: z.string().required(),\r\n }),\r\n cwd: z.string().default(process.cwd()),\r\n tools: z.boolean().default(true),\r\n}) as unknown) as Config\r\n\r\nexport const name = 'tmim'\r\n\r\nexport async function apply(ctx: Context, config: Config): Promise<void> {\r\n if (config.webhook.enabled && !config.webhook.secret) {\r\n throw new Error('[tmim] webhook.enabled requires webhook.secret (the TM IM webhook_secret)')\r\n }\r\n\r\n const client = new TmimClient({\r\n baseUrl: config.baseUrl,\r\n ...(config.credential.kind === 'apikey' ? { token: config.credential.token } : {}),\r\n deviceId: config.deviceId,\r\n })\r\n\r\n // Authenticate up-front so misconfiguration fails loudly at boot.\r\n if (config.credential.kind === 'password') {\r\n await client.login(config.credential.username, config.credential.password)\r\n ctx.logger.info('[tmim] authenticated via password login')\r\n } else {\r\n client.setToken(config.credential.token)\r\n ctx.logger.info('[tmim] using Agent api key')\r\n }\r\n\r\n // Optionally drive Agent Takeover configuration.\r\n if (config.enableAgentTakeover) {\r\n const setup = await client.configureAgentTakeover({\r\n ...(config.webhook.enabled\r\n ? { pushMode: 1 as const, endpointUrl: config.webhook.path }\r\n : { pushMode: 3 as const }), // WEBHOOK or PULL\r\n capabilities: ['text'],\r\n modelInfo: config.agent.model,\r\n })\r\n if (setup.webhookSecret && config.webhook.enabled) {\r\n ctx.logger.warn(\r\n '[tmim] NEW webhook_secret issued (shown only once) — save it and set webhook.secret. ' +\r\n 'Rotate anytime via POST /v1/me/agent-takeover/rotate-webhook-secret',\r\n )\r\n }\r\n }\r\n\r\n const bridge = new TmimBridge(ctx, {\r\n getClient: () => client,\r\n agentOptions: { provider: config.agent.provider, model: config.agent.model },\r\n cwd: config.cwd,\r\n })\r\n\r\n // Register all resources in one effect so disposal unwinds them in order.\r\n ctx.effect(function* () {\r\n if (config.webhook.enabled) {\r\n yield mountTmimWebhook(ctx, {\r\n path: config.webhook.path,\r\n secret: config.webhook.secret,\r\n maxSkewSeconds: config.webhook.maxSkewSeconds,\r\n handle: (event: WebhookEvent) => handleWebhookEvent(bridge, event),\r\n })\r\n }\r\n\r\n if (config.tools) {\r\n yield registerTmimTools(ctx, () => client)\r\n }\r\n\r\n yield bridge.start()\r\n ctx.logger.info('[tmim] started (webhook=%s, tools=%s)', config.webhook.enabled, config.tools)\r\n }, 'tmim')\r\n}\r\n\r\nfunction handleWebhookEvent(bridge: TmimBridge, event: WebhookEvent): void {\r\n if (event.event !== 'message.created' && event.event !== 'mention.created') return\r\n const data = event.data as { sender?: { actor_type?: number } } & Record<string, any>\r\n // Skip messages the agent itself sent (actor_type === 2 is an Agent).\r\n if (data.sender?.actor_type === 2) return\r\n const replyTo = Number(data.reply_to ?? 0) || undefined\r\n void bridge.ingestMessage({\r\n convId: Number(data.conv_id),\r\n senderId: Number(data.sender_id),\r\n text: data.content?.text,\r\n ...(replyTo === undefined ? {} : { replyToMessageId: replyTo }),\r\n })\r\n}\r\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,64 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@tmim/dsh-plugin-tmim",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@tmim/dsh-plugin-tmim",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "DeepSeek Harness plugin that connects to the TM IM network (Agent Takeover webhook + REST).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.mts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.mts",
|
|
11
|
+
"default": "./dist/index.mjs"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"README.md",
|
|
18
|
+
"README.zh-CN.md"
|
|
19
|
+
],
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsdown",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"test": "vitest run",
|
|
24
|
+
"test:signature": "vitest run tests/verify-signature.test.ts"
|
|
25
|
+
},
|
|
26
|
+
"keywords": [
|
|
27
|
+
"dsh-plugin",
|
|
28
|
+
"deepseek-harness",
|
|
29
|
+
"tmim",
|
|
30
|
+
"im",
|
|
31
|
+
"messaging",
|
|
32
|
+
"cordis"
|
|
33
|
+
],
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@deepseek-ai/cordis": "^4.1.0",
|
|
37
|
+
"@deepseek-ai/schemastery": "^3.19.0",
|
|
38
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
39
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
|
|
40
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1",
|
|
41
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
|
|
42
|
+
"@deepseek-ai/dsh-host-webserver": "^0.0.1-rc.1"
|
|
43
|
+
},
|
|
44
|
+
"peerDependenciesMeta": {
|
|
45
|
+
"@deepseek-ai/dsh-llm": {
|
|
46
|
+
"optional": true
|
|
47
|
+
},
|
|
48
|
+
"@deepseek-ai/dsh-session": {
|
|
49
|
+
"optional": true
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@deepseek-ai/cordis": "^4.1.0",
|
|
54
|
+
"@deepseek-ai/schemastery": "^3.19.0",
|
|
55
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
56
|
+
"@deepseek-ai/dsh-llm": "^0.0.1-rc.1",
|
|
57
|
+
"@deepseek-ai/dsh-session": "^0.0.1-rc.1",
|
|
58
|
+
"@deepseek-ai/dsh-tools": "^0.0.1-rc.1",
|
|
59
|
+
"@deepseek-ai/dsh-host-webserver": "^0.0.1-rc.1",
|
|
60
|
+
"typescript": "^5.6.0",
|
|
61
|
+
"tsdown": "^0.23.5",
|
|
62
|
+
"vitest": "^2.1.0"
|
|
63
|
+
}
|
|
64
|
+
}
|