dsh-plugin-discourse 1.0.0 → 1.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/README.md CHANGED
@@ -1,10 +1,14 @@
1
1
  # dsh-plugin-discourse
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/dsh-plugin-discourse.svg)](https://www.npmjs.com/package/dsh-plugin-discourse)
4
+ [![Publish to npm](https://github.com/AkarinLiu/dsh-plugin-discourse/actions/workflows/publish.yml/badge.svg)](https://github.com/AkarinLiu/dsh-plugin-discourse/actions/workflows/publish.yml)
5
+
3
6
  Discourse administration and agent tools for the [DeepSeek Harness](https://github.com/deepseek-ai).
4
7
 
5
- One Host plugin registers 16 model-facing tools over the Discourse REST API: an agent can search a
6
- forum, read topics and profiles, publish posts, and carry out the moderator and administrator work a
7
- staff account would do in the web UI.
8
+ One Host plugin registers 17 model-facing tools over the Discourse REST API: an agent can search a
9
+ forum, read topics and profiles, publish posts, carry out the moderator and administrator work a
10
+ staff account would do in the web UI, and sign in through the user's browser with a scoped user API
11
+ key.
8
12
 
9
13
  ```
10
14
  User → DSH agent → discourse_* tools → Discourse REST API → forum
@@ -14,6 +18,7 @@ User → DSH agent → discourse_* tools → Discourse REST API → forum
14
18
 
15
19
  | Tool | Group | What it does | Key needed |
16
20
  |---|---|---|---|
21
+ | `discourse_login` | auth | Start browser authorization, report the session, finish an older payload page, cancel a pending attempt, or sign out | no |
17
22
  | `discourse_search` | read | Search topics, posts, and users | no |
18
23
  | `discourse_topics` | read | List a feed (`latest`/`new`/`unread`/`top`) or one category | no |
19
24
  | `discourse_topic` | read | Read a topic and a slice of its posts | no |
@@ -37,27 +42,62 @@ thread search never floods the model context. Results keep ids, so a follow-up c
37
42
  ## Install
38
43
 
39
44
  The bundle is a profile plugin: it is installed into one DSH profile and selected in that profile's
40
- bundle list.
45
+ bundle list. Install it through the Web UI when you can: DSH's bundle installer replaces an existing
46
+ copy or local link before it writes the package, while the CLI is a thin pnpm wrapper.
41
47
 
42
- **From the Web UI** — open *Settings → Plugins → Install bundle* and give it the package directory
43
- (for a clone) or a package spec such as `github:AkarinLiu/dsh-plugin-discourse`. DSH installs the
44
- package, adds it to the profile's bundle list, and activates it.
48
+ **From the Web UI (recommended)** — open *Settings → Plugins → Install bundle* and enter
49
+ `dsh-plugin-discourse` for the npm package, or `github:AkarinLiu/dsh-plugin-discourse`, or a local
50
+ clone path. DSH installs the package, adds it to the profile's bundle list, and activates it.
45
51
 
46
- **From the CLI** — the launcher passes everything after `plugin` to pnpm:
52
+ **From npm on the CLI** — the launcher passes everything after `plugin` to pnpm, so this is a plain
53
+ pnpm install rather than the bundle installer:
47
54
 
48
55
  ```bash
49
- gh repo clone AkarinLiu/dsh-plugin-discourse
50
- dsh plugin --profile desktop add ./dsh-plugin-discourse
56
+ dsh plugin --profile desktop add dsh-plugin-discourse
51
57
  ```
52
58
 
53
- That installs the package; select `dsh-plugin-discourse` once in the Plugins page (or through the
59
+ After a CLI install, select `dsh-plugin-discourse` once in the Plugins page (or through the
54
60
  `plugin_manager` tool) so the profile records it as a bundle.
55
61
 
62
+ **From a clone** — `gh repo clone AkarinLiu/dsh-plugin-discourse`, then
63
+ `dsh plugin --profile desktop add ./dsh-plugin-discourse`. On Windows this creates a directory
64
+ junction in the profile; read the next section before moving that profile to the npm package.
65
+
56
66
  To remove it, uninstall the bundle from the Plugins page, which drops the dependency and the bundle
57
67
  entry together.
58
68
 
59
69
  > The plugin is Host-only: no build step, no dependencies, no client assets.
60
70
 
71
+ ### Windows: EPERM when replacing a local link
72
+
73
+ A clone install leaves `node_modules\dsh-plugin-discourse` as a directory junction. pnpm's hoisted
74
+ linker does not always record or prune that junction, so a later npm install cannot replace it and
75
+ Windows refuses the rename:
76
+
77
+ ```text
78
+ [ERR_PNPM_EPERM] [importPackage ...\node_modules\dsh-plugin-discourse]
79
+ EPERM: operation not permitted, rename '...dsh-plugin-discourse_tmp_...' -> '...dsh-plugin-discourse'
80
+ ```
81
+
82
+ The checkout is untouched; only the link is stale. From a clone of this repository, let the bundled
83
+ doctor remove the link and then reinstall:
84
+
85
+ ```bash
86
+ node scripts/doctor.mjs --profile desktop --fix
87
+ dsh plugin --profile desktop add dsh-plugin-discourse
88
+ ```
89
+
90
+ Or delete the junction by hand and retry:
91
+
92
+ ```powershell
93
+ $link = "$env:USERPROFILE\.dsh\profiles\desktop\node_modules\dsh-plugin-discourse"
94
+ if ((Get-Item $link -Force -ErrorAction SilentlyContinue).LinkType -eq 'Junction') {
95
+ (Get-Item $link -Force).Delete()
96
+ }
97
+ ```
98
+
99
+ Restart the Harness after the package is replaced so the new module generation loads.
100
+
61
101
  ## Configure
62
102
 
63
103
  The bundle ships its defaults in `cordis.patch.yml`, which DSH applies as the bundle layer. To point
@@ -73,6 +113,11 @@ it at your forum, add an override to your **profile's** `cordis.patch.yml` (`$DS
73
113
  readOnly: true
74
114
  timeoutMs: 20000
75
115
  maxItems: 12
116
+ authMode: auto
117
+ loginScopes: read,write,session_info
118
+ loginClientId: dsh-plugin-discourse
119
+ loginApplicationName: DeepSeek Harness
120
+ loginTimeoutMs: 600000
76
121
  ```
77
122
 
78
123
  A loader override replaces the row's `config` wholesale, so restate every key you want to keep.
@@ -85,6 +130,11 @@ A loader override replaces the row's `config` wholesale, so restate every key yo
85
130
  | `readOnly` | `false` | Blocks every mutating tool when `true`. |
86
131
  | `timeoutMs` | `20000` | Per-request deadline, 1000–120000. |
87
132
  | `maxItems` | `12` | Default listing size, 1–50. |
133
+ | `authMode` | `auto` | `auto` uses a stored browser session for normal calls and `apiKey` for staff/admin calls; `api-key` ignores the session; `user` requires it for every call. |
134
+ | `loginScopes` | `read,write,session_info` | Comma-separated scopes requested by browser sign-in. Narrow or extend this to match the forum's `allow_user_api_key_scopes`. |
135
+ | `loginClientId` | `dsh-plugin-discourse` | Client id Discourse records with the authorized key. |
136
+ | `loginApplicationName` | `DeepSeek Harness` | Application name shown on the forum authorization page. |
137
+ | `loginTimeoutMs` | `600000` | Browser sign-in deadline, 30000–1800000. Discourse's device code lives 10 minutes. |
88
138
 
89
139
  Anonymous reads work out of the box against the default forum, so you can try `discourse_topics`
90
140
  before configuring anything.
@@ -106,11 +156,54 @@ export DISCOURSE_API_USERNAME="staff-bot"
106
156
 
107
157
  Row config wins over the environment when both are present.
108
158
 
159
+ ### Browser sign-in
160
+
161
+ `discourse_login` obtains a **user API key** for the signed-in forum account, so the plugin can act as
162
+ that user without an admin key in the profile. On current Discourse releases it uses the device
163
+ authorization flow (`Auth API` version 4):
164
+
165
+ 1. `discourse_login` with `action: "start"` returns a verification URL and an eight-character code.
166
+ 2. Open the URL, sign in to the forum if needed, and confirm the code.
167
+ 3. `discourse_login` with `action: "status"` reports the signed-in account.
168
+
169
+ The tool also accepts `waitSeconds` on `status` (0–120) when the caller wants to wait for the browser
170
+ step before reporting. `action: "cancel"` withdraws a pending attempt without touching a stored
171
+ session.
172
+
173
+ Forums older than the device flow get the classic `/user-api-key/new` page instead. The plugin asks
174
+ for the encrypted payload that page shows after approval, and
175
+ `discourse_login` with `action: "complete"` and the pasted `payload` finishes the sign-in. Neither
176
+ path needs an `auth_redirect`, a callback listener, or a change to
177
+ `allowed_user_api_auth_redirects`.
178
+
179
+ The authorized key is stored as the `discourse/session` record in the DSH credential store — never in
180
+ `cordis.patch.yml` and never in a tool result. `discourse_login` with `action: "logout"` revokes it at
181
+ the forum when possible and deletes the local record. `readOnly` still permits sign-in and sign-out:
182
+ they manage authentication, not forum content.
183
+
184
+ `authMode` decides which credential a request uses:
185
+
186
+ - `auto` (default) — normal reads and posts use the stored session; staff/admin tools keep using
187
+ `apiKey` when one is configured, so moderation and administration still work.
188
+ - `api-key` — ignore any stored session and always use `apiKey`.
189
+ - `user` — require the stored session for every call; requests fail with a sign-in hint when it is
190
+ missing or expired.
191
+
192
+ A user API key is limited to the account's own permissions and the scopes granted on the consent
193
+ page. It cannot call admin endpoints such as `/admin/users.json`; those tools need `apiKey` (or fail
194
+ with the forum's 403). A forum that does not allow `write` in `allow_user_api_key_scopes` needs
195
+ `loginScopes: read,session_info`.
196
+
109
197
  ## Safety
110
198
 
111
- - `readOnly: true` is enforced inside the plugin, before any mutating request is built.
199
+ - `readOnly: true` blocks every content-mutating tool before any forum request is built. Browser
200
+ sign-in and sign-out remain available because they manage the credential rather than content.
201
+ - `discourse_login` stores the user API key in the credential store and never prints it. Use
202
+ `action: "logout"` to revoke it at the forum and remove the local record.
112
203
  - `discourse_moderate_user` and the site-settings tools require an admin-scoped key; a key without
113
- those scopes receives the forum's 403 response, and the tool reports it instead of retrying.
204
+ those scopes receives the forum's 403 response, and the tool reports it instead of retrying. A
205
+ browser session does not widen a user key into an admin key; `authMode: auto` therefore prefers a
206
+ configured `apiKey` for staff and admin calls.
114
207
  - Failures carry the HTTP status and Discourse's own error text, so a wrong id or a missing scope is
115
208
  visible in the transcript.
116
209
  - Writes should be exercised with a staging forum first — `discourse_delete_post` with
@@ -121,22 +214,72 @@ Row config wins over the environment when both are present.
121
214
  The package is plain ESM with no runtime dependencies; `index.js` is the whole plugin.
122
215
 
123
216
  ```bash
124
- node --check index.js # syntax
125
- node scripts/smoke.mjs # register every tool and validate definitions offline
126
- node scripts/smoke.mjs --live # additionally call read-only tools against the configured forum
217
+ node --check index.js # syntax
218
+ npm test # register every tool, validate definitions, and exercise browser login offline
219
+ node scripts/smoke.mjs --live # additionally call read-only tools against the forum
220
+ node scripts/login-test.mjs # browser login flow against a stubbed forum and credential store
221
+ npm run verify # build the npm tarball and check its file list
222
+ node scripts/doctor.mjs --profile desktop # inspect a profile for a stale local link
127
223
  ```
128
224
 
129
225
  `scripts/smoke.mjs` drives `apply()` with a stub context, so it exercises the real registration path
130
- without starting a Harness.
226
+ without starting a Harness. `scripts/login-test.mjs` drives the same `apply()` path with a stub
227
+ credential store, authorization seam, and forum: it starts the classic page, completes it with an RSA
228
+ payload, reads the status, and signs out, then repeats the device-code path. `scripts/verify-package.mjs`
229
+ packs the package with `npm pack` and fails when a runtime file is missing from `files`, when the
230
+ manifest loses `dsh.bundle`, or when a locale loses its display metadata. `prepack` runs the smoke and
231
+ login tests, so a broken package cannot be published.
232
+
233
+ ## Release
234
+
235
+ Releases are automated by [`.github/workflows/publish.yml`](.github/workflows/publish.yml): every
236
+ push to `main` or a `v*` tag, and every pull request, verifies the package, and a `v*` tag publishes it to
237
+ [npmjs.com](https://www.npmjs.com/package/dsh-plugin-discourse) with
238
+ [provenance](https://docs.npmjs.com/generating-provenance-statements).
239
+
240
+ 1. Bump `version` in `package.json` (the tag must match it exactly).
241
+ 2. Commit, then tag and push:
242
+
243
+ ```bash
244
+ git tag -a v1.1.0 -m "dsh-plugin-discourse 1.1.0"
245
+ git push origin main --follow-tags
246
+ ```
247
+
248
+ 3. The workflow runs on Node 24, verifies the tarball, checks `v<version>`, and publishes with
249
+ `--provenance --access public`.
250
+
251
+ Before the first release, add a **Trusted Publisher** on npmjs.com (package page → *Settings* →
252
+ *Trusted Publishing* → *GitHub Actions*):
253
+
254
+ | Field | Value |
255
+ |---|---|
256
+ | Repository | `AkarinLiu/dsh-plugin-discourse` |
257
+ | Workflow file | `publish.yml` |
258
+ | Environment | *(leave empty)* |
259
+ | Allowed action | `npm publish` |
260
+
261
+ With the trusted publisher in place the workflow needs no long-lived credential: it exchanges the
262
+ GitHub OIDC token (`id-token: write`) for publish rights and attaches provenance. Until that setting
263
+ exists, the publish job falls back to the repository's `NPM_TOKEN` secret, and it fails with a
264
+ diagnostic when neither credential is available. npm now requires an interactive 2FA challenge for
265
+ token and trusted-publisher management, so the one-time setup happens in the browser; after it, tagged
266
+ releases are fully automatic.
131
267
 
132
268
  ## 中文说明
133
269
 
134
- - 这是一个 **Host 插件**,向 DSH 智能体提供 16 个 Discourse 工具(读取 / 发帖 / 版主 / 管理员)。
135
- - 安装:在 DSH Web UI 的 *设置 → 插件 → 安装 bundle* 中选择本仓库目录,或填入 `github:AkarinLiu/dsh-plugin-discourse`。
270
+ - 这是一个 **Host 插件**,向 DSH 智能体提供 17 个 Discourse 工具(登录 / 读取 / 发帖 / 版主 / 管理员)。
271
+ - 浏览器授权登录:先调用 `discourse_login`(`action: "start"`),插件会返回论坛授权页地址和 8 位验证码;在浏览器中登录论坛并确认验证码后,再用 `action: "status"` 查看登录结果。中途放弃可用 `action: "cancel"`。新版 Discourse 使用设备授权码流程;旧版论坛会显示加密 payload,把该 payload 交给 `action: "complete"` 即可完成。两种方式都不需要配置 `auth_redirect`,也不需要修改 `allowed_user_api_auth_redirects`。
272
+ - 登录得到的用户 API Key 保存在 DSH 凭据存储的 `discourse/session` 记录中,不会写进 `cordis.patch.yml`,也不会出现在工具结果里;`action: "logout"` 会尽量在论坛撤销它并删除本地记录。
273
+ - `authMode: auto`(默认)下,普通读取和发帖使用已登录的会话,版主/管理员工具继续使用 `apiKey`;改为 `api-key` 可完全忽略会话,改为 `user` 则所有请求都必须先登录。
274
+ - 用户 API Key 只拥有该账号的权限,不能调用 `/admin/*` 管理接口;管理工具仍需 `apiKey`。如果论坛不允许 `write` scope,请把 `loginScopes` 调成 `read,session_info`。
275
+ - 安装:优先用 Web UI 的 *设置 → 插件 → 安装 bundle*,填入 `dsh-plugin-discourse`(npm)或 `github:AkarinLiu/dsh-plugin-discourse`;安装器会先替换旧的本地链接。
276
+ - 命令行:`dsh plugin --profile desktop add dsh-plugin-discourse` 只是把参数转交给 pnpm,安装后还需在插件页选中该 bundle。
277
+ - Windows 从本地克隆切到 npm 时报 `ERR_PNPM_EPERM ... rename ...dsh-plugin-discourse...`:这是 profile 里遗留的目录联接;运行 `node scripts/doctor.mjs --profile desktop --fix` 后再安装(脚本只删链接,不动源码),然后重启 Harness。
278
+ - 发布:推送 `v*` 标签会触发 `.github/workflows/publish.yml`。首次发布前在 npmjs.com 的包设置 → *Trusted Publishing* 添加 GitHub Actions(仓库 `AkarinLiu/dsh-plugin-discourse`、工作流 `publish.yml`、环境留空、允许 `npm publish`),之后不需要 `NPM_TOKEN`,由 OIDC 发布并生成 provenance。
136
279
  - 配置:在 profile 的 `cordis.patch.yml` 中加一条 `id: discourse` 的覆盖,把 `baseUrl` 改成你的论坛并填入 `apiKey`(覆盖会整体替换 `config`,请把其它键一并写全)。
137
- - 不需要 API Key 即可试用只读工具(默认指向 `https://meta.discourse.org`)。
280
+ - 不需要 API Key 即可试用只读工具(默认指向 `https://meta.discourse.org`),也可以直接用浏览器登录获得用户会话。
138
281
  - 密钥建议通过环境变量 `DISCOURSE_API_KEY` / `DISCOURSE_API_USERNAME` 提供,避免写进配置文件。
139
- - 不需要写入权限时把 `readOnly` 设为 `true`,所有写操作会被插件直接拒绝。
282
+ - 不需要写入权限时把 `readOnly` 设为 `true`,所有写操作会被插件直接拒绝;登录和退出仍可使用。
140
283
 
141
284
  ## License
142
285
 
package/cordis.patch.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  # Inserts the Discourse tool bundle into the current DSH profile.
2
2
  # Change `baseUrl` to your forum and point `apiKey` at an admin-scoped key
3
- # before enabling writes.
3
+ # before enabling writes. Browser sign-in stores a scoped user API key in the
4
+ # DSH credential store, so no key is required in this file for `discourse_login`.
4
5
  - insert:
5
6
  - id: discourse
6
7
  name: dsh-plugin-discourse
@@ -11,3 +12,8 @@
11
12
  readOnly: false
12
13
  timeoutMs: 20000
13
14
  maxItems: 12
15
+ authMode: auto
16
+ loginScopes: read,write,session_info
17
+ loginClientId: dsh-plugin-discourse
18
+ loginApplicationName: DeepSeek Harness
19
+ loginTimeoutMs: 600000