dsh-github-router 0.1.0 → 0.3.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/CHANGELOG.md +19 -1
- package/README.md +19 -9
- package/README.zh.md +16 -6
- package/docs/design.md +40 -34
- package/lib/client.js +161 -161
- package/lib/config.js +4 -3
- package/lib/index.js +8 -8
- package/lib/settings.js +38 -0
- package/package.json +14 -7
- package/lib/remote.js +0 -169
package/CHANGELOG.md
CHANGED
|
@@ -5,7 +5,25 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [
|
|
8
|
+
## [0.3.0] - 2026-08-28
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Adapted to DeepSeek Harness 0.1.2-alpha.1
|
|
13
|
+
|
|
14
|
+
## [0.2.1] - 2026-08-28
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Declare compatibility for dsh 0.1.1-rc.2
|
|
19
|
+
|
|
20
|
+
## [0.2.0] - 2026-08-21
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- **Migrated plugin configuration to the framework's plugin-settings mechanism** (requires DSH ≥ 0.1.0-rc.7, which serves every registered settings namespace and dispatches plugin-owned configuration cards). The browser half now registers a `settings.plugin.item` card keyed by the `dsh-github-router` namespace inside Settings → Plugins, bound through `ctx.settingsScope` (shared describe mirror, revision fencing, recovery reads, reconnect invalidation); the plugin-owned `/dsh-github-router/config` web routes and the standalone `settings.section` page are removed. The Host keeps the same `settings.register` seam and adds `applies: 'live'`.
|
|
25
|
+
- The token control is now a true write-only field: the configured state comes from the describe mirror's secret slot list (badge: "已配置/configured"), a typed value writes the token, and a blank save clears a configured one.
|
|
26
|
+
- Runtime requirements: the settings card needs DSH ≥ 0.1.0-rc.7; the Host-side settings seam alone still works on older compositions (tools resolve the composition config as before).
|
|
9
27
|
|
|
10
28
|
## [0.1.0] - 2026-08-19
|
|
11
29
|
|
package/README.md
CHANGED
|
@@ -46,6 +46,15 @@ dsh plugin --profile web add github:<owner>/dsh-github-router
|
|
|
46
46
|
|
|
47
47
|
Then **restart the DSH backend** — the host composition loads at process start. The tools appear in new sessions: `github_probe`, `github_pr`, `github_issue`, `github_file`, `github_api`, plus the `github-router` skill.
|
|
48
48
|
|
|
49
|
+
## Compatibility
|
|
50
|
+
|
|
51
|
+
Choose the plugin version that matches your DeepSeek Harness release:
|
|
52
|
+
|
|
53
|
+
| DeepSeek Harness | Install |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| 0.1.1 or earlier | `dsh-github-router@0.2.1` |
|
|
56
|
+
| 0.1.2-alpha or later | the latest `dsh-github-router` |
|
|
57
|
+
|
|
49
58
|
## Usage
|
|
50
59
|
|
|
51
60
|
Agent side:
|
|
@@ -76,13 +85,14 @@ Behavior notes:
|
|
|
76
85
|
|
|
77
86
|
## Configuration
|
|
78
87
|
|
|
79
|
-
Settings → **GitHub Router**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
overridden by the user are badged, and blank fields
|
|
83
|
-
defaults below. The
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
Settings → **Plugins** shows the **GitHub Router** card on the configurable
|
|
89
|
+
tab (the framework's `settings.plugin.item` card slot keyed by the settings
|
|
90
|
+
namespace; requires DSH ≥ 0.1.0-rc.7): edits are staged locally and written
|
|
91
|
+
only on save, fields overridden by the user are badged, and blank fields
|
|
92
|
+
fall back to the defaults below. The token is a write-only field — a blank
|
|
93
|
+
save clears a configured token. The same values can be set in the
|
|
94
|
+
composition (profile `cordis.patch.yml`) as the plugin's base config; the
|
|
95
|
+
Settings UI overrides per user.
|
|
86
96
|
|
|
87
97
|
| Field | Default | Meaning |
|
|
88
98
|
| --- | --- | --- |
|
|
@@ -112,8 +122,8 @@ per user.
|
|
|
112
122
|
| Path | Purpose |
|
|
113
123
|
| ---- | ------- |
|
|
114
124
|
| `cordis.patch.yml` | Profile patch layer inserting the `dsh-github-router` row |
|
|
115
|
-
| `lib/index.js` | Host plugin: settings
|
|
116
|
-
| `lib/client.js` | Browser half: the
|
|
125
|
+
| `lib/index.js` | Host plugin: settings namespace, five tools, skill, guidance |
|
|
126
|
+
| `lib/client.js` | Browser half: the settings card (hand-written factory bundle, no build step) |
|
|
117
127
|
| `lib/config.js` | Settings schema, defaults, runtime option resolution |
|
|
118
128
|
| `lib/net.js`, `lib/tunnel.js` | Route-aware HTTP layer; zero-dependency CONNECT proxy tunnel |
|
|
119
129
|
| `lib/routes/` | One module per route: `api` (GET-only REST), `gh` (CLI), `git` (protocol), `html` (page parse), `mirror` (raw mirrors) |
|
package/README.zh.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
- **一次调用、结构化结果** —— PR 一次返回元数据、讨论、评审、提交、变更文件与 diff,每个部件标注来源路由;失败时返回路由矩阵,而不是十几条重试过的终端命令。
|
|
16
16
|
- **连通性探针** —— `github_probe` 一次调用报告宿主侧哪些路由存活,附耗时与推荐路由链。
|
|
17
17
|
- **零运行时依赖** —— peer 依赖由 DSH profile 解析;代理请求走插件自带的 CONNECT 隧道(不依赖第三方 HTTP 栈),因此可以完全离线安装。
|
|
18
|
-
-
|
|
18
|
+
- **框架级插件设置卡片** —— 设置 → **Plugins(插件)** 的可配置页中出现「GitHub 路由」卡片(框架的 `settings.plugin.item` 机制,按设置命名空间配对,需 DSH ≥ 0.1.0-rc.7):`dsh-github-router` 设置命名空间(secret token、代理、路由开关、缓存 TTL、字节上限),编辑暂存、保存/放弃、覆盖徽标、token 已配置徽标。
|
|
19
19
|
- **上下文效率** —— 按类别 TTL 的响应缓存、全局字节上限、带截断注记的列表上限,以及限流余量提示。
|
|
20
20
|
|
|
21
21
|
## 环境要求
|
|
@@ -48,6 +48,15 @@ dsh plugin --profile web add github:<owner>/dsh-github-router
|
|
|
48
48
|
`github_probe`、`github_pr`、`github_issue`、`github_file`、`github_api`,
|
|
49
49
|
以及 `github-router` 技能。
|
|
50
50
|
|
|
51
|
+
## 兼容性
|
|
52
|
+
|
|
53
|
+
请选择与你的 DeepSeek Harness 版本匹配的插件版本:
|
|
54
|
+
|
|
55
|
+
| DeepSeek Harness | 安装 |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| 0.1.1 及更早 | `dsh-github-router@0.2.1` |
|
|
58
|
+
| 0.1.2-alpha 及更新 | 最新版 `dsh-github-router` |
|
|
59
|
+
|
|
51
60
|
## 使用
|
|
52
61
|
|
|
53
62
|
Agent 侧:
|
|
@@ -78,11 +87,12 @@ github_api { path: "/repos/o/r/commits", query: { per_page: 5 } }
|
|
|
78
87
|
|
|
79
88
|
## 配置
|
|
80
89
|
|
|
81
|
-
设置 → **
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
90
|
+
设置 → **Plugins(插件)** → 可配置页中的「GitHub 路由」卡片(注册进
|
|
91
|
+
`settings.plugin.item` 槽、以设置命名空间为 key 的框架插件卡片,需要
|
|
92
|
+
DSH ≥ 0.1.0-rc.7):编辑内容先本地暂存、点保存才落盘,被用户覆盖的
|
|
93
|
+
字段带标记,留空字段回退到下方默认值;token 为只写字段,留空保存会
|
|
94
|
+
清除已配置的 token。同样的值也可以在组合(profile 的
|
|
95
|
+
`cordis.patch.yml`)中作为插件基础配置写入;设置 UI 按用户覆盖。
|
|
86
96
|
|
|
87
97
|
| 字段 | 默认 | 含义 |
|
|
88
98
|
| --- | --- | --- |
|
package/docs/design.md
CHANGED
|
@@ -107,44 +107,50 @@ substitute bytes. When enabled, each configured base yields two candidates
|
|
|
107
107
|
per file (raw passthrough and the `github.com` `/raw/` route), tried in
|
|
108
108
|
order.
|
|
109
109
|
|
|
110
|
-
## Settings
|
|
110
|
+
## Settings card
|
|
111
111
|
|
|
112
112
|
The Host registers the `dsh-github-router` settings namespace on the
|
|
113
113
|
official settings seam (durable document, schema validation, revision
|
|
114
|
-
fencing)
|
|
115
|
-
factory bundle with no build step) registers
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
the
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
114
|
+
fencing, `applies: 'live'`). The browser half (`lib/client.js`, a
|
|
115
|
+
hand-written ModuleLoader factory bundle with no build step) registers one
|
|
116
|
+
plugin configuration CARD into the `settings.plugin.item` slot keyed by the
|
|
117
|
+
namespace — the framework's mechanism for plugins distributed outside the
|
|
118
|
+
repository (DSH ≥ 0.1.0-rc.7): the Settings → Plugins configurable tab
|
|
119
|
+
reads which namespaces the Host serves and dispatches the intersection of
|
|
120
|
+
that ledger and the registered cards, so the card appears only when the
|
|
121
|
+
namespace is registered and served.
|
|
122
|
+
|
|
123
|
+
### Framework transport
|
|
124
|
+
|
|
125
|
+
Since DSH 0.1.0-rc.7 the api-proxy serves every registered settings
|
|
126
|
+
namespace (the earlier `WEB_SETTINGS_NAMESPACES` allowlist and its
|
|
127
|
+
`settings-not-exposed` answer retired in PR #2404), so the card binds the
|
|
128
|
+
namespace through the framework settings transport:
|
|
129
|
+
`ctx.settingsScope.bind({ namespace })` returns a `SettingsScope` whose
|
|
130
|
+
snapshot carries the redacted resolved section
|
|
131
|
+
(`status`/`value`/`base`/`user`/`revision`/`writable`). All consumers
|
|
132
|
+
derive from one shared describe mirror, so reads never block activation,
|
|
133
|
+
writes carry revision fencing and recovery re-reads, and the mirror
|
|
134
|
+
refreshes on `settings/document-updated` and `connection/reset` — a Host
|
|
135
|
+
restart no longer strands the form in a failed state. No plugin-owned HTTP
|
|
136
|
+
route exists.
|
|
137
|
+
|
|
138
|
+
### Card form
|
|
139
|
+
|
|
140
|
+
Edits are staged locally and written only on save, one field per
|
|
141
|
+
`scope.set`/`scope.unset`; after each write the card verifies the user
|
|
142
|
+
layer (JSON-shaped deep equality) and keeps drafts that did not land.
|
|
143
|
+
Secret fields never ride a response, so the token control is write-only:
|
|
144
|
+
its configured state comes from the describe mirror's secret slot list
|
|
145
|
+
(the snapshot itself redacts it from every layer), a typed value writes the
|
|
146
|
+
token through `settings.mutate` (the one direction secrets cross the wire),
|
|
147
|
+
and a blank draft clears a configured one.
|
|
148
|
+
|
|
149
|
+
Because the scope writes **scalar fields by name**, the settings schema is
|
|
150
|
+
deliberately flat (`routesApi`, `cacheTtlMeta`, …); the Host projects it
|
|
151
|
+
into the nested runtime shape (`routes.api`, `cacheTtlSeconds.meta`) in
|
|
146
152
|
`resolveOptions`. The composition layer can still carry the same flat
|
|
147
|
-
keys. The
|
|
153
|
+
keys. The card shows the common fields up top (token, proxy, main route
|
|
148
154
|
switches) and the long tail (timeouts, retries, cache TTLs, mirrors,
|
|
149
155
|
repos, git cache dir) in a collapsed "Advanced settings" disclosure.
|
|
150
156
|
|
package/lib/client.js
CHANGED
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-github-router — client half (hand-written factory bundle, no build step).
|
|
3
3
|
*
|
|
4
|
-
* One
|
|
5
|
-
* the `settings.
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* correctly.
|
|
4
|
+
* One plugin configuration CARD inside the framework's Settings → Plugins →
|
|
5
|
+
* configurable tab, registered into the `settings.plugin.item` slot keyed by
|
|
6
|
+
* the settings namespace — the framework mechanism for plugins distributed
|
|
7
|
+
* outside the repository (DSH ≥ 0.1.0-rc.7): the Host registers the
|
|
8
|
+
* namespace, the browser registers the card under the same key, and the tab
|
|
9
|
+
* pairs the two. No plugin-owned HTTP route exists anymore; reads and writes
|
|
10
|
+
* ride the framework settings transport (`ctx.settingsScope`).
|
|
12
11
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* The form keeps the staged-editing model: edits are staged locally and
|
|
13
|
+
* written only on save; each write goes through the bound settings scope
|
|
14
|
+
* (revision fencing, recovery reads on failure) and the card verifies the
|
|
15
|
+
* user layer afterwards, keeping drafts that did not land. Secret fields
|
|
16
|
+
* (the GitHub token) never ride a response, so the token control is
|
|
17
|
+
* write-only: it starts blank, a typed value writes the token, and a blank
|
|
18
|
+
* draft clears a configured one — the configured state comes from the
|
|
19
|
+
* describe mirror's secret slot list.
|
|
16
20
|
*
|
|
17
21
|
* The module requires only `react` and
|
|
18
|
-
* `@deepseek-ai/dsh-client-
|
|
19
|
-
*
|
|
20
|
-
* framework locale service under the `dsh-github-router`
|
|
22
|
+
* `@deepseek-ai/dsh-client-store` (the shared browser snapshot-store seed —
|
|
23
|
+
* the only runtime API it uses is `createSnapshotStore`). All copy goes
|
|
24
|
+
* through the framework locale service under the `dsh-github-router`
|
|
25
|
+
* namespace.
|
|
21
26
|
*/
|
|
22
27
|
window.__ModuleLoader__.load({
|
|
23
28
|
id: 'dsh-github-router',
|
|
@@ -25,12 +30,12 @@ window.__ModuleLoader__.load({
|
|
|
25
30
|
var module = { exports: {} };
|
|
26
31
|
var exports = module.exports;
|
|
27
32
|
var React = require('react');
|
|
28
|
-
var runtime = require('@deepseek-ai/dsh-client-
|
|
33
|
+
var runtime = require('@deepseek-ai/dsh-client-store');
|
|
34
|
+
|
|
35
|
+
var NAMESPACE = 'dsh-github-router';
|
|
36
|
+
var NS = 'dsh-github-router';
|
|
29
37
|
|
|
30
38
|
// ------------------------------------------------------------ specs
|
|
31
|
-
// Flat scalar fields only: the client settings scope writes one field
|
|
32
|
-
// per call (`scope.set(field, value)`), so nested schema objects are
|
|
33
|
-
// deliberately absent from this page.
|
|
34
39
|
function textSpec(field) {
|
|
35
40
|
return {
|
|
36
41
|
field: field,
|
|
@@ -83,7 +88,7 @@ window.__ModuleLoader__.load({
|
|
|
83
88
|
|
|
84
89
|
// ------------------------------------------------------ field catalog
|
|
85
90
|
var PRIMARY_FIELDS = [
|
|
86
|
-
{ field: 'token', kind: '
|
|
91
|
+
{ field: 'token', kind: 'secret', spec: textSpec('token'), labelKey: 'field.token', hintKey: 'hint.token' },
|
|
87
92
|
{ field: 'tokenEnv', kind: 'text', spec: textSpec('tokenEnv'), labelKey: 'field.tokenEnv', hintKey: 'hint.tokenEnv' },
|
|
88
93
|
{ field: 'proxy', kind: 'text', spec: textSpec('proxy'), labelKey: 'field.proxy', hintKey: 'hint.proxy' },
|
|
89
94
|
{ field: 'routesApi', kind: 'bool', spec: boolSpec('routesApi'), labelKey: 'field.routesApi', hintKey: null },
|
|
@@ -108,12 +113,10 @@ window.__ModuleLoader__.load({
|
|
|
108
113
|
FIELDS.forEach(function (f) { SPECS[f.field] = f.spec; });
|
|
109
114
|
|
|
110
115
|
// ------------------------------------------------------------- i18n
|
|
111
|
-
var NS = 'dsh-github-router';
|
|
112
116
|
var zhDict = {
|
|
113
117
|
'nav': 'GitHub 路由',
|
|
114
118
|
'description': 'GitHub 读取的路由与缓存配置。',
|
|
115
119
|
'advanced': '高级设置',
|
|
116
|
-
'unavailable': '无法从宿主加载配置。',
|
|
117
120
|
'save': '保存',
|
|
118
121
|
'saving': '保存中…',
|
|
119
122
|
'discard': '放弃修改',
|
|
@@ -121,6 +124,7 @@ window.__ModuleLoader__.load({
|
|
|
121
124
|
'readOnly': '当前部署不允许修改设置。',
|
|
122
125
|
'invalid': '无效值',
|
|
123
126
|
'overridden': '已覆盖',
|
|
127
|
+
'configured': '已配置',
|
|
124
128
|
'field.token': 'GitHub Token',
|
|
125
129
|
'field.tokenEnv': 'Token 环境变量',
|
|
126
130
|
'field.proxy': '代理地址',
|
|
@@ -138,7 +142,7 @@ window.__ModuleLoader__.load({
|
|
|
138
142
|
'field.mirrors': '镜像基址(逗号分隔)',
|
|
139
143
|
'field.repos': '本地仓库白名单(逗号分隔)',
|
|
140
144
|
'field.gitCacheDir': 'git 缓存目录',
|
|
141
|
-
'hint.token': '
|
|
145
|
+
'hint.token': '仅写入:留空并保存 = 清除已配置的 token;优先使用 tokenEnv。',
|
|
142
146
|
'hint.tokenEnv': '指向 token 的环境变量名,例如 GITHUB_TOKEN。',
|
|
143
147
|
'hint.proxy': '空 = 继承环境代理;direct = 永不代理;其余为代理 URL。',
|
|
144
148
|
'hint.maxBytes': '每个响应体的字节上限(16384..8388608)。',
|
|
@@ -153,7 +157,6 @@ window.__ModuleLoader__.load({
|
|
|
153
157
|
'nav': 'GitHub Router',
|
|
154
158
|
'description': 'Routing and cache configuration for GitHub reads.',
|
|
155
159
|
'advanced': 'Advanced settings',
|
|
156
|
-
'unavailable': 'The configuration could not be loaded from the host.',
|
|
157
160
|
'save': 'Save',
|
|
158
161
|
'saving': 'Saving…',
|
|
159
162
|
'discard': 'Discard',
|
|
@@ -161,6 +164,7 @@ window.__ModuleLoader__.load({
|
|
|
161
164
|
'readOnly': 'This deployment does not allow settings edits.',
|
|
162
165
|
'invalid': 'invalid',
|
|
163
166
|
'overridden': 'overridden',
|
|
167
|
+
'configured': 'configured',
|
|
164
168
|
'field.token': 'GitHub token',
|
|
165
169
|
'field.tokenEnv': 'Token env var',
|
|
166
170
|
'field.proxy': 'Proxy URL',
|
|
@@ -178,7 +182,7 @@ window.__ModuleLoader__.load({
|
|
|
178
182
|
'field.mirrors': 'Mirror bases (comma-separated)',
|
|
179
183
|
'field.repos': 'Local repo allowlist (comma-separated)',
|
|
180
184
|
'field.gitCacheDir': 'Git cache directory',
|
|
181
|
-
'hint.token': '
|
|
185
|
+
'hint.token': 'Write-only: a blank save clears the configured token; prefer tokenEnv.',
|
|
182
186
|
'hint.tokenEnv': 'Environment variable naming the token, e.g. GITHUB_TOKEN.',
|
|
183
187
|
'hint.proxy': "Empty = inherit ambient proxy env; 'direct' = never proxy; anything else is a proxy URL.",
|
|
184
188
|
'hint.maxBytes': 'Byte cap per response body (16384..8388608).',
|
|
@@ -191,100 +195,22 @@ window.__ModuleLoader__.load({
|
|
|
191
195
|
};
|
|
192
196
|
|
|
193
197
|
// ------------------------------------------------------------- form
|
|
194
|
-
/**
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
* uses plain same-origin fetch (GET = redacted view, POST = writes).
|
|
198
|
-
* The framework's settings scope is NOT used — its api-proxy serves
|
|
199
|
-
* settings namespaces through a hardcoded allowlist that third-party
|
|
200
|
-
* plugins cannot join.
|
|
201
|
-
*/
|
|
202
|
-
|
|
203
|
-
/** One `/dsh-github-router/config` call (GET or POST). */
|
|
204
|
-
function configCall(payload) {
|
|
205
|
-
var init = payload === undefined
|
|
206
|
-
? { method: 'GET', cache: 'no-store' }
|
|
207
|
-
: {
|
|
208
|
-
method: 'POST',
|
|
209
|
-
cache: 'no-store',
|
|
210
|
-
headers: { 'content-type': 'application/json' },
|
|
211
|
-
body: JSON.stringify(payload),
|
|
212
|
-
};
|
|
213
|
-
return fetch('/dsh-github-router/config', init).then(function (response) {
|
|
214
|
-
return response.json().then(function (envelope) {
|
|
215
|
-
if (!envelope || envelope.ok !== true) {
|
|
216
|
-
var message = envelope && envelope.error !== undefined ? String(envelope.error) : 'config call failed (HTTP ' + response.status + ')';
|
|
217
|
-
throw new Error('dsh-github-router: ' + message);
|
|
218
|
-
}
|
|
219
|
-
return envelope.value;
|
|
220
|
-
});
|
|
221
|
-
});
|
|
198
|
+
/** JSON-shaped deep equality for verifying writes against the user layer. */
|
|
199
|
+
function sameJson(a, b) {
|
|
200
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
222
201
|
}
|
|
223
202
|
|
|
224
|
-
/**
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
value: view && typeof view.value === 'object' ? view.value : {},
|
|
236
|
-
base: view && typeof view.base === 'object' ? view.base : {},
|
|
237
|
-
user: view && typeof view.user === 'object' ? view.user : {},
|
|
238
|
-
revision: view && view.revision,
|
|
239
|
-
writable: !!(view && view.writable),
|
|
240
|
-
mode: 'host',
|
|
241
|
-
};
|
|
242
|
-
publish();
|
|
243
|
-
}
|
|
244
|
-
function fail(message) {
|
|
245
|
-
snapshot = { status: 'unavailable', value: {}, base: {}, user: {}, revision: undefined, writable: false, mode: 'memory' };
|
|
246
|
-
console.error('[dsh-github-router] config load failed', message);
|
|
247
|
-
publish();
|
|
248
|
-
}
|
|
249
|
-
function load() {
|
|
250
|
-
return configCall().then(accept, fail);
|
|
251
|
-
}
|
|
252
|
-
function write(op) {
|
|
253
|
-
return configCall({ ops: [op], expectedRevision: snapshot.revision }).then(accept, function (error) {
|
|
254
|
-
// A rejected write may mean the document moved (revision conflict)
|
|
255
|
-
// or storage failed — re-read so the next save fences correctly.
|
|
256
|
-
return load().then(function () { throw error; });
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
load();
|
|
261
|
-
return {
|
|
262
|
-
getSnapshot: function () { return snapshot; },
|
|
263
|
-
subscribe: function (fn) {
|
|
264
|
-
listeners.add(fn);
|
|
265
|
-
return function () { listeners.delete(fn); };
|
|
266
|
-
},
|
|
267
|
-
set: function (field, value) {
|
|
268
|
-
return write({ op: 'set', path: [field], value: value });
|
|
269
|
-
},
|
|
270
|
-
unset: function (field) {
|
|
271
|
-
return write({ op: 'unset', path: [field] });
|
|
272
|
-
},
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
/** A static scope stub for environments without fetch. */
|
|
277
|
-
function unavailableScope() {
|
|
278
|
-
var snapshot = { status: 'unavailable', value: {}, base: {}, user: {}, revision: undefined, writable: false, mode: 'memory' };
|
|
279
|
-
return {
|
|
280
|
-
getSnapshot: function () { return snapshot; },
|
|
281
|
-
subscribe: function () { return function () {}; },
|
|
282
|
-
set: function () { return Promise.resolve(); },
|
|
283
|
-
unset: function () { return Promise.resolve(); },
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function GithubRouterForm(scope) {
|
|
203
|
+
/**
|
|
204
|
+
* Stages one card's edits over the plugin's settings namespace and writes
|
|
205
|
+
* them on save. The scope is the framework-bound settings scope
|
|
206
|
+
* (`ctx.settingsScope.bind({ namespace })`): its snapshot carries the
|
|
207
|
+
* redacted resolved section (`status`/`value`/`base`/`user`/`revision`/
|
|
208
|
+
* `writable`), and `set`/`unset` write one field each with revision
|
|
209
|
+
* fencing and recovery reads. The describe mirror supplements the
|
|
210
|
+
* snapshot with the secret slot list, the only signal that a redacted
|
|
211
|
+
* field (the token) is configured.
|
|
212
|
+
*/
|
|
213
|
+
function GithubRouterForm(scope, mirror) {
|
|
288
214
|
var staged = new Map(); // field -> { text, clear }
|
|
289
215
|
var saving = false;
|
|
290
216
|
var failed = false;
|
|
@@ -311,6 +237,17 @@ window.__ModuleLoader__.load({
|
|
|
311
237
|
var s = snap();
|
|
312
238
|
return !!(s.user && typeof s.user === 'object' && Object.prototype.hasOwnProperty.call(s.user, field));
|
|
313
239
|
}
|
|
240
|
+
/** Whether a redacted secret slot holds a value (from the describe mirror). */
|
|
241
|
+
function secretSet(field) {
|
|
242
|
+
if (mirror === null || typeof mirror.namespace !== 'function') return false;
|
|
243
|
+
var row = mirror.namespace(NAMESPACE);
|
|
244
|
+
if (row === undefined || !Array.isArray(row.secrets)) return false;
|
|
245
|
+
for (var i = 0; i < row.secrets.length; i++) {
|
|
246
|
+
var slot = row.secrets[i];
|
|
247
|
+
if (Array.isArray(slot.path) && slot.path.length === 1 && slot.path[0] === field) return slot.set === true;
|
|
248
|
+
}
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
314
251
|
|
|
315
252
|
function publish() {
|
|
316
253
|
var s = snap();
|
|
@@ -323,14 +260,26 @@ window.__ModuleLoader__.load({
|
|
|
323
260
|
var invalidField = false;
|
|
324
261
|
if (stagedEntry !== undefined) {
|
|
325
262
|
text = stagedEntry.text;
|
|
326
|
-
|
|
327
|
-
|
|
263
|
+
if (stagedEntry.clear) {
|
|
264
|
+
overridden = false;
|
|
265
|
+
} else {
|
|
266
|
+
var parsedStaged = f.spec.parse(stagedEntry.text);
|
|
267
|
+
// The badge previews the save outcome: a staged blank that
|
|
268
|
+
// parses to a clear is not an override anymore.
|
|
269
|
+
overridden = parsedStaged !== undefined && parsedStaged.kind === 'set';
|
|
270
|
+
invalidField = parsedStaged === undefined;
|
|
271
|
+
}
|
|
328
272
|
} else {
|
|
329
|
-
text = f.spec.format(sectionValue(f.field));
|
|
330
|
-
overridden = stored(f.field);
|
|
273
|
+
text = f.kind === 'secret' ? '' : f.spec.format(sectionValue(f.field));
|
|
274
|
+
overridden = f.kind === 'secret' ? secretSet(f.field) : stored(f.field);
|
|
331
275
|
}
|
|
332
276
|
if (invalidField) invalid = true;
|
|
333
|
-
fields[f.field] = {
|
|
277
|
+
fields[f.field] = {
|
|
278
|
+
text: text,
|
|
279
|
+
overridden: overridden,
|
|
280
|
+
invalid: invalidField,
|
|
281
|
+
configured: f.kind === 'secret' && stagedEntry === undefined ? secretSet(f.field) : undefined,
|
|
282
|
+
};
|
|
334
283
|
});
|
|
335
284
|
store.set({
|
|
336
285
|
available: s.status === 'ready',
|
|
@@ -343,6 +292,7 @@ window.__ModuleLoader__.load({
|
|
|
343
292
|
});
|
|
344
293
|
}
|
|
345
294
|
scope.subscribe(publish);
|
|
295
|
+
if (mirror !== null && typeof mirror.subscribe === 'function') mirror.subscribe(publish);
|
|
346
296
|
publish();
|
|
347
297
|
|
|
348
298
|
function edit(field, text) {
|
|
@@ -368,9 +318,42 @@ window.__ModuleLoader__.load({
|
|
|
368
318
|
staged.forEach(function (stagedEntry, field) {
|
|
369
319
|
var spec = SPECS[field];
|
|
370
320
|
if (stagedEntry.clear) {
|
|
321
|
+
if (field === 'token') {
|
|
322
|
+
// A blank draft clears the token only when one is configured.
|
|
323
|
+
if (secretSet(field)) {
|
|
324
|
+
writes.push({
|
|
325
|
+
field: field,
|
|
326
|
+
run: function () { return scope.unset(field).then(function () { return !secretSet(field); }); },
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
371
331
|
if (stored(field)) {
|
|
372
|
-
writes.push({
|
|
332
|
+
writes.push({
|
|
333
|
+
field: field,
|
|
334
|
+
run: function () { return scope.unset(field).then(function () { return !stored(field); }); },
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (field === 'token') {
|
|
340
|
+
var tokenText = stagedEntry.text.trim();
|
|
341
|
+
if (tokenText === '') {
|
|
342
|
+
// A blank draft clears the token when one is configured.
|
|
343
|
+
if (secretSet(field)) {
|
|
344
|
+
writes.push({
|
|
345
|
+
field: field,
|
|
346
|
+
run: function () { return scope.unset(field).then(function () { return !secretSet(field); }); },
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return;
|
|
373
350
|
}
|
|
351
|
+
writes.push({
|
|
352
|
+
field: field,
|
|
353
|
+
run: (function (value) {
|
|
354
|
+
return function () { return scope.set(field, value).then(function () { return secretSet(field); }); };
|
|
355
|
+
})(tokenText),
|
|
356
|
+
});
|
|
374
357
|
return;
|
|
375
358
|
}
|
|
376
359
|
if (stagedEntry.text === spec.format(sectionValue(field))) return;
|
|
@@ -380,9 +363,22 @@ window.__ModuleLoader__.load({
|
|
|
380
363
|
return;
|
|
381
364
|
}
|
|
382
365
|
if (write.kind === 'clear') {
|
|
383
|
-
writes.push({
|
|
366
|
+
writes.push({
|
|
367
|
+
field: field,
|
|
368
|
+
run: function () { return scope.unset(field).then(function () { return !stored(field); }); },
|
|
369
|
+
});
|
|
384
370
|
} else {
|
|
385
|
-
writes.push({
|
|
371
|
+
writes.push({
|
|
372
|
+
field: field,
|
|
373
|
+
run: (function (value) {
|
|
374
|
+
return function () {
|
|
375
|
+
return scope.set(field, value).then(function () {
|
|
376
|
+
var current = snap().user;
|
|
377
|
+
return !!(current && typeof current === 'object' && Object.prototype.hasOwnProperty.call(current, field) && sameJson(current[field], value));
|
|
378
|
+
});
|
|
379
|
+
};
|
|
380
|
+
})(write.value),
|
|
381
|
+
});
|
|
386
382
|
}
|
|
387
383
|
});
|
|
388
384
|
if (blocked || writes.length === 0) return;
|
|
@@ -393,7 +389,7 @@ window.__ModuleLoader__.load({
|
|
|
393
389
|
var chain = Promise.resolve();
|
|
394
390
|
writes.forEach(function (w) {
|
|
395
391
|
chain = chain.then(function () {
|
|
396
|
-
return w.run().
|
|
392
|
+
return w.run().then(function (ok) { if (ok !== true) landed = false; }, function () { landed = false; });
|
|
397
393
|
});
|
|
398
394
|
});
|
|
399
395
|
chain.then(function () {
|
|
@@ -406,9 +402,9 @@ window.__ModuleLoader__.load({
|
|
|
406
402
|
|
|
407
403
|
function inject(t) {
|
|
408
404
|
return {
|
|
409
|
-
// The
|
|
410
|
-
//
|
|
411
|
-
// `
|
|
405
|
+
// The slots runtime synthesizes a `use<Key>` hook for every entry
|
|
406
|
+
// of this `hooks` object (the shipped plugin cards' `hooks` →
|
|
407
|
+
// `useBashCard` contract).
|
|
412
408
|
hooks: { settings: store },
|
|
413
409
|
t: t,
|
|
414
410
|
edit: edit,
|
|
@@ -420,8 +416,8 @@ window.__ModuleLoader__.load({
|
|
|
420
416
|
return { inject: inject, store: store, save: save, edit: edit, discard: discard, resetField: resetField };
|
|
421
417
|
}
|
|
422
418
|
|
|
423
|
-
// -------------------------------------------------------------
|
|
424
|
-
var
|
|
419
|
+
// ------------------------------------------------------------- card
|
|
420
|
+
var cardStyle = { padding: '4px 0 16px' };
|
|
425
421
|
var gridStyle = {
|
|
426
422
|
display: 'grid',
|
|
427
423
|
gridTemplateColumns: 'minmax(180px, 1fr) minmax(180px, 1fr)',
|
|
@@ -447,12 +443,16 @@ window.__ModuleLoader__.load({
|
|
|
447
443
|
cursor: 'pointer',
|
|
448
444
|
};
|
|
449
445
|
|
|
450
|
-
function
|
|
446
|
+
function GithubRouterCard(props) {
|
|
451
447
|
var state = props.useSettings(function (s) { return s; });
|
|
452
448
|
var t = props.t;
|
|
453
449
|
|
|
450
|
+
// The framework cards render nothing until their namespace is served;
|
|
451
|
+
// this card follows the same posture.
|
|
452
|
+
if (!state.available) return null;
|
|
453
|
+
|
|
454
454
|
function renderField(f) {
|
|
455
|
-
var fieldState = state.fields[f.field] || { text: '', overridden: false, invalid: false };
|
|
455
|
+
var fieldState = state.fields[f.field] || { text: '', overridden: false, invalid: false, configured: false };
|
|
456
456
|
if (f.kind === 'bool') {
|
|
457
457
|
return React.createElement(
|
|
458
458
|
'label',
|
|
@@ -466,15 +466,22 @@ window.__ModuleLoader__.load({
|
|
|
466
466
|
t(f.labelKey),
|
|
467
467
|
);
|
|
468
468
|
}
|
|
469
|
+
var marker;
|
|
470
|
+
if (f.kind === 'secret') {
|
|
471
|
+
marker = fieldState.configured === true
|
|
472
|
+
? React.createElement('span', { style: { marginLeft: '6px', opacity: 0.6 } }, t('configured'))
|
|
473
|
+
: null;
|
|
474
|
+
} else {
|
|
475
|
+
marker = fieldState.overridden
|
|
476
|
+
? React.createElement('span', { style: { marginLeft: '6px', opacity: 0.6 } }, t('overridden'))
|
|
477
|
+
: null;
|
|
478
|
+
}
|
|
469
479
|
return React.createElement(
|
|
470
480
|
'label',
|
|
471
481
|
{ key: f.field, style: rowStyle, title: f.hintKey ? t(f.hintKey) : undefined },
|
|
472
|
-
React.createElement('span', { style: labelStyle },
|
|
473
|
-
t(f.labelKey),
|
|
474
|
-
fieldState.overridden ? React.createElement('span', { style: { marginLeft: '6px', opacity: 0.6 } }, t('overridden')) : null,
|
|
475
|
-
),
|
|
482
|
+
React.createElement('span', { style: labelStyle }, t(f.labelKey), marker),
|
|
476
483
|
React.createElement('input', {
|
|
477
|
-
type: f.kind === 'number' ? 'number' : 'text',
|
|
484
|
+
type: f.kind === 'number' ? 'number' : f.kind === 'secret' ? 'password' : 'text',
|
|
478
485
|
value: fieldState.text,
|
|
479
486
|
disabled: !state.writable,
|
|
480
487
|
style: inputStyle,
|
|
@@ -486,18 +493,17 @@ window.__ModuleLoader__.load({
|
|
|
486
493
|
|
|
487
494
|
return React.createElement(
|
|
488
495
|
'section',
|
|
489
|
-
{ style:
|
|
496
|
+
{ style: cardStyle },
|
|
490
497
|
React.createElement('p', { style: { margin: '0 0 12px', opacity: 0.75, fontSize: '13px' } }, t('description')),
|
|
491
|
-
!state.
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
state.available ? React.createElement(
|
|
498
|
+
!state.writable ? React.createElement('div', { style: { color: '#c00', fontSize: '13px', marginBottom: '8px' } }, t('readOnly')) : null,
|
|
499
|
+
React.createElement('div', { style: gridStyle }, PRIMARY_FIELDS.map(renderField)),
|
|
500
|
+
React.createElement(
|
|
495
501
|
'details',
|
|
496
502
|
{ style: { marginTop: '12px' } },
|
|
497
503
|
React.createElement('summary', { style: { cursor: 'pointer', fontSize: '13px', opacity: 0.85 } }, t('advanced')),
|
|
498
504
|
React.createElement('div', { style: Object.assign({ marginTop: '8px' }, gridStyle) }, ADVANCED_FIELDS.map(renderField)),
|
|
499
|
-
)
|
|
500
|
-
|
|
505
|
+
),
|
|
506
|
+
React.createElement('footer', { style: footerStyle },
|
|
501
507
|
React.createElement('button', {
|
|
502
508
|
style: buttonStyle,
|
|
503
509
|
disabled: !state.dirty || state.invalid || state.saving,
|
|
@@ -509,13 +515,13 @@ window.__ModuleLoader__.load({
|
|
|
509
515
|
onClick: props.discard,
|
|
510
516
|
}, t('discard')),
|
|
511
517
|
state.failed ? React.createElement('span', { style: { color: '#c00', fontSize: '13px' } }, t('failed')) : null,
|
|
512
|
-
)
|
|
518
|
+
),
|
|
513
519
|
);
|
|
514
520
|
}
|
|
515
521
|
|
|
516
522
|
// ------------------------------------------------------------- plugin
|
|
517
523
|
var name = 'dsh-github-router';
|
|
518
|
-
var inject = ['slots', 'locale'];
|
|
524
|
+
var inject = ['slots', 'locale', 'settingsScope', 'connection'];
|
|
519
525
|
function apply(ctx) {
|
|
520
526
|
console.info('[dsh-github-router] client bundle loaded');
|
|
521
527
|
try {
|
|
@@ -523,24 +529,18 @@ window.__ModuleLoader__.load({
|
|
|
523
529
|
ctx.effect(function () {
|
|
524
530
|
ctx.locale.register(NS, { zh: zhDict, en: enDict });
|
|
525
531
|
}, 'dsh-github-router: locale dictionaries');
|
|
526
|
-
var
|
|
527
|
-
ctx.
|
|
532
|
+
var scope = ctx.settingsScope.bind({ namespace: NAMESPACE });
|
|
533
|
+
var mirror = typeof ctx.settingsScope.describe === 'function' ? ctx.settingsScope.describe() : null;
|
|
534
|
+
var form = new GithubRouterForm(scope, mirror);
|
|
535
|
+
ctx.slots.inject('settings.plugin.item', function* () {
|
|
528
536
|
yield ctx.slots.register({
|
|
529
|
-
name: 'settings.
|
|
530
|
-
|
|
531
|
-
order: 65,
|
|
532
|
-
label: function () { return t('nav'); },
|
|
537
|
+
name: 'settings.plugin.item',
|
|
538
|
+
key: NAMESPACE,
|
|
533
539
|
locale: NS,
|
|
534
|
-
inject: function () {
|
|
535
|
-
|
|
536
|
-
var scope = typeof fetch === 'function' ? new RemoteScope() : unavailableScope();
|
|
537
|
-
form = new GithubRouterForm(scope);
|
|
538
|
-
}
|
|
539
|
-
return form.inject(t);
|
|
540
|
-
},
|
|
541
|
-
}, GithubRouterSection);
|
|
540
|
+
inject: function () { return form.inject(t); },
|
|
541
|
+
}, GithubRouterCard);
|
|
542
542
|
});
|
|
543
|
-
console.info('[dsh-github-router] settings
|
|
543
|
+
console.info('[dsh-github-router] settings card registered');
|
|
544
544
|
} catch (error) {
|
|
545
545
|
console.error('[dsh-github-router] client apply failed', error);
|
|
546
546
|
}
|
package/lib/config.js
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Settings schema and runtime-option resolution for dsh-github-router.
|
|
3
3
|
*
|
|
4
4
|
* The settings section (`dsh-github-router` namespace) is registered through
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* the official settings seam (`ctx.settings.register`, see `lib/settings.js`);
|
|
6
|
+
* the Settings UI renders a plugin card for it (Settings → Plugins, DSH ≥
|
|
7
|
+
* 0.1.0-rc.7). Secrets are declared with `.role('secret')` so they are
|
|
8
|
+
* redacted on every wire boundary and rendered as write-only inputs.
|
|
8
9
|
*
|
|
9
10
|
* The schema is deliberately FLAT: the client settings scope writes scalar
|
|
10
11
|
* fields by name (`scope.set(field, value)`), so nested objects cannot be
|
package/lib/index.js
CHANGED
|
@@ -19,14 +19,15 @@
|
|
|
19
19
|
* 4. Plugin-owned state (fetch cache, response cache) lives under
|
|
20
20
|
* <DSH_HOME>/storages/dsh-github-router; user repositories are only ever
|
|
21
21
|
* read (log/diff/show) and only when explicitly granted.
|
|
22
|
-
* 5. Configuration rides the official settings seam
|
|
23
|
-
* namespace: durable document, schema validation,
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* 5. Configuration rides the official settings seam end to end
|
|
23
|
+
* (`dsh-github-router` namespace: durable document, schema validation,
|
|
24
|
+
* revision fencing). Since DSH 0.1.0-rc.7 the framework serves every
|
|
25
|
+
* registered settings namespace to the browser and renders the plugin's
|
|
26
|
+
* configuration card in Settings → Plugins, so no plugin-owned HTTP
|
|
27
|
+
* route exists.
|
|
27
28
|
* @module dsh-github-router
|
|
28
29
|
*/
|
|
29
|
-
import {
|
|
30
|
+
import { installSettings } from './settings.js'
|
|
30
31
|
import { registerGuidance } from './guidance.js'
|
|
31
32
|
import { registerSkill } from './skill.js'
|
|
32
33
|
import { registerApiTool } from './tools/api.js'
|
|
@@ -39,8 +40,7 @@ export const name = 'dsh-github-router'
|
|
|
39
40
|
export const inject = ['tools', 'subprocess', 'skills', 'systemPrompt']
|
|
40
41
|
|
|
41
42
|
export function apply(ctx, config = {}) {
|
|
42
|
-
const
|
|
43
|
-
const options = remote.options
|
|
43
|
+
const options = installSettings(ctx, config).options
|
|
44
44
|
|
|
45
45
|
registerSkill(ctx)
|
|
46
46
|
registerGuidance(ctx)
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side settings wiring for dsh-github-router.
|
|
3
|
+
*
|
|
4
|
+
* The `dsh-github-router` namespace rides the official settings seam
|
|
5
|
+
* (`ctx.settings.register`): the framework persists the section in the user
|
|
6
|
+
* settings document, validates it against the schema, fences writes with
|
|
7
|
+
* revisions, and redacts secret-role fields on every wire boundary.
|
|
8
|
+
*
|
|
9
|
+
* Since DSH 0.1.0-rc.7 the api-proxy serves every registered settings
|
|
10
|
+
* namespace to the (loopback) browser and the Settings → Plugins section
|
|
11
|
+
* dispatches a plugin-owned card for it, so the client half binds the same
|
|
12
|
+
* namespace through `ctx.settingsScope` and the plugin needs no HTTP routes
|
|
13
|
+
* of its own. This module only registers the namespace and exposes the
|
|
14
|
+
* runtime-options thunk the tools consume; a settings change therefore
|
|
15
|
+
* applies to the NEXT tool call.
|
|
16
|
+
* @module dsh-github-router/settings
|
|
17
|
+
*/
|
|
18
|
+
import { Config, NAMESPACE, resolveOptions } from './config.js'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Register the settings namespace and return the runtime-options thunk.
|
|
22
|
+
* @param ctx - the plugin context.
|
|
23
|
+
* @param config - the composition-layer config subset (the `base` layer).
|
|
24
|
+
*/
|
|
25
|
+
export function installSettings(ctx, config) {
|
|
26
|
+
const holder = { scope: null }
|
|
27
|
+
|
|
28
|
+
ctx.inject(['settings'], (sctx) => {
|
|
29
|
+
holder.scope = sctx.settings.register(NAMESPACE, Config, { base: config, applies: 'live' })
|
|
30
|
+
sctx.effect(() => () => {
|
|
31
|
+
holder.scope = null
|
|
32
|
+
}, 'dsh-github-router: settings scope teardown')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
options: () => (holder.scope !== null ? resolveOptions(holder.scope.get()) : resolveOptions(config)),
|
|
37
|
+
}
|
|
38
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-github-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "DeepSeek Harness plugin: read-only GitHub access for agents (github_probe / github_pr / github_issue / github_file / github_api). Routes every request inside the tool — api.github.com (direct or proxy), gh CLI, git protocol (plugin-owned fetch cache plus read-only local repo reads), PR/issue page HTML with strict JSON embeddedData extraction, and optional user-configured raw mirrors — so agents never burn turns fighting sandbox TLS/proxy failures in a shell. No write/push capability exists anywhere in the plugin.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh-plugin",
|
|
@@ -46,13 +46,20 @@
|
|
|
46
46
|
"docs"
|
|
47
47
|
],
|
|
48
48
|
"dsh": {
|
|
49
|
+
"compatibility": {
|
|
50
|
+
"node": ">=20",
|
|
51
|
+
"dshReleases": {
|
|
52
|
+
"0.1.2-alpha.1": "compatible",
|
|
53
|
+
"0.1.1-rc.2": "incompatible"
|
|
54
|
+
}
|
|
55
|
+
},
|
|
49
56
|
"bundle": {
|
|
50
57
|
"patch": "./cordis.patch.yml"
|
|
51
58
|
},
|
|
52
59
|
"client": {
|
|
53
60
|
"platform": "web",
|
|
54
61
|
"inject": [
|
|
55
|
-
"@deepseek-ai/dsh-client-
|
|
62
|
+
"@deepseek-ai/dsh-client-store"
|
|
56
63
|
]
|
|
57
64
|
}
|
|
58
65
|
},
|
|
@@ -61,14 +68,14 @@
|
|
|
61
68
|
},
|
|
62
69
|
"peerDependencies": {
|
|
63
70
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
64
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
65
|
-
"@deepseek-ai/dsh-settings": "^0.1.
|
|
66
|
-
"@deepseek-ai/dsh-credentials": "^0.1.
|
|
67
|
-
"@deepseek-ai/schemastery": "^
|
|
71
|
+
"@deepseek-ai/dsh-tools": "^0.1.2-alpha.1",
|
|
72
|
+
"@deepseek-ai/dsh-settings": "^0.1.2-alpha.1",
|
|
73
|
+
"@deepseek-ai/dsh-credentials": "^0.1.2-alpha.1",
|
|
74
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
68
75
|
},
|
|
69
76
|
"devDependencies": {
|
|
70
77
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
71
|
-
"@deepseek-ai/dsh-tools": "^0.1.
|
|
78
|
+
"@deepseek-ai/dsh-tools": "^0.1.2-alpha.1"
|
|
72
79
|
},
|
|
73
80
|
"publishConfig": {
|
|
74
81
|
"access": "public"
|
package/lib/remote.js
DELETED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Host-side configuration routes for the browser settings page.
|
|
3
|
-
*
|
|
4
|
-
* The channel is the dshmarket pattern: the plugin mounts its own HTTP
|
|
5
|
-
* routes on the shared web server (`ctx.webServer.register`, prefix
|
|
6
|
-
* `/dsh-github-router`) and the browser page talks to them with plain
|
|
7
|
-
* same-origin fetch — no typert, no gateway, no framework allowlist. The
|
|
8
|
-
* routes are backed by the SAME official settings seam: the namespace
|
|
9
|
-
* stays durable in the settings document, with schema validation,
|
|
10
|
-
* revision fencing, and secret redaction.
|
|
11
|
-
*
|
|
12
|
-
* Mutating requests are POST-only, same-origin-checked (Origin === Host)
|
|
13
|
-
* and body-capped, mirroring dsh-market's route security posture.
|
|
14
|
-
* @module dsh-github-router/remote
|
|
15
|
-
*/
|
|
16
|
-
import { Config, NAMESPACE, resolveOptions } from './config.js'
|
|
17
|
-
|
|
18
|
-
const ROUTE_PREFIX = '/dsh-github-router'
|
|
19
|
-
const MAX_BODY_BYTES = 16384
|
|
20
|
-
|
|
21
|
-
function sendJson(response, status, payload) {
|
|
22
|
-
response.writeHead(status, {
|
|
23
|
-
'cache-control': 'no-store',
|
|
24
|
-
'content-type': 'application/json; charset=utf-8',
|
|
25
|
-
})
|
|
26
|
-
response.end(JSON.stringify(payload))
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Same-origin enforcement for mutating routes (dsh-market posture). */
|
|
30
|
-
function sameOrigin(request) {
|
|
31
|
-
const origin = request.headers.origin
|
|
32
|
-
const host = request.headers.host
|
|
33
|
-
if (origin === undefined || host === undefined) return false
|
|
34
|
-
try {
|
|
35
|
-
return new URL(origin).host === host
|
|
36
|
-
} catch {
|
|
37
|
-
return false
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/** Read and parse a JSON request body with a hard byte cap. */
|
|
42
|
-
async function readJsonBody(request, maxBytes = MAX_BODY_BYTES) {
|
|
43
|
-
const chunks = []
|
|
44
|
-
let size = 0
|
|
45
|
-
for await (const chunk of request) {
|
|
46
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
47
|
-
size += buffer.length
|
|
48
|
-
if (size > maxBytes) throw new Error('request body too large')
|
|
49
|
-
chunks.push(buffer)
|
|
50
|
-
}
|
|
51
|
-
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Install the settings-namespace registration, the options thunk, and the
|
|
56
|
-
* `/dsh-github-router/config` routes in one wiring. Returns the
|
|
57
|
-
* runtime-options thunk the tools consume.
|
|
58
|
-
*/
|
|
59
|
-
export function installRemote(ctx, config) {
|
|
60
|
-
const holder = { scope: null, settings: null }
|
|
61
|
-
|
|
62
|
-
ctx.inject(['settings'], (sctx) => {
|
|
63
|
-
holder.settings = sctx.settings
|
|
64
|
-
holder.scope = sctx.settings.register(NAMESPACE, Config, { base: config })
|
|
65
|
-
sctx.effect(() => () => {
|
|
66
|
-
holder.scope = null
|
|
67
|
-
holder.settings = null
|
|
68
|
-
}, 'dsh-github-router: settings scope teardown')
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
/** Redacted wire view of this namespace (value/base/user/revision/writable). */
|
|
72
|
-
function viewOf() {
|
|
73
|
-
if (holder.scope === null || holder.settings === null) return null
|
|
74
|
-
const descriptors = holder.settings.describe({ redactSecrets: true })
|
|
75
|
-
const descriptor = descriptors.find((d) => String(d.ns) === NAMESPACE)
|
|
76
|
-
if (descriptor === undefined) return null
|
|
77
|
-
return {
|
|
78
|
-
value: descriptor.value ?? {},
|
|
79
|
-
base: descriptor.base ?? {},
|
|
80
|
-
user: descriptor.user ?? {},
|
|
81
|
-
revision: descriptor.revision,
|
|
82
|
-
writable: holder.settings.writable === true,
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function validateOps(payload) {
|
|
87
|
-
if (payload === null || typeof payload !== 'object' || !Array.isArray(payload.ops) || payload.ops.length === 0) {
|
|
88
|
-
return 'payload.ops must be a non-empty array'
|
|
89
|
-
}
|
|
90
|
-
if (payload.ops.length > 40) return 'too many ops in one write'
|
|
91
|
-
for (const op of payload.ops) {
|
|
92
|
-
if (op === null || typeof op !== 'object' || (op.op !== 'set' && op.op !== 'unset')) {
|
|
93
|
-
return 'each op must be { op: "set" | "unset", path: [field] }'
|
|
94
|
-
}
|
|
95
|
-
if (!Array.isArray(op.path) || op.path.length !== 1 || typeof op.path[0] !== 'string' || op.path[0].length === 0 || op.path[0].length > 64) {
|
|
96
|
-
return 'op.path must be a single field name'
|
|
97
|
-
}
|
|
98
|
-
if (op.op === 'set' && op.value === undefined) return 'set ops require a value'
|
|
99
|
-
}
|
|
100
|
-
return null
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const routeHandler = async (request, response) => {
|
|
104
|
-
const pathname = new URL(request.url ?? '/', 'http://x').pathname
|
|
105
|
-
if (pathname !== ROUTE_PREFIX + '/config') {
|
|
106
|
-
sendJson(response, 404, { ok: false, error: 'unknown dsh-github-router route' })
|
|
107
|
-
return
|
|
108
|
-
}
|
|
109
|
-
if (request.method === 'GET') {
|
|
110
|
-
if (holder.scope === null || holder.settings === null) {
|
|
111
|
-
sendJson(response, 503, { ok: false, error: 'the settings service is not mounted in this deployment' })
|
|
112
|
-
return
|
|
113
|
-
}
|
|
114
|
-
const view = viewOf()
|
|
115
|
-
if (view === null) {
|
|
116
|
-
sendJson(response, 503, { ok: false, error: 'settings namespace is not registered' })
|
|
117
|
-
return
|
|
118
|
-
}
|
|
119
|
-
sendJson(response, 200, { ok: true, value: view })
|
|
120
|
-
return
|
|
121
|
-
}
|
|
122
|
-
if (request.method === 'POST') {
|
|
123
|
-
if (!sameOrigin(request)) {
|
|
124
|
-
sendJson(response, 403, { ok: false, error: 'configuration writes are limited to same-origin requests' })
|
|
125
|
-
return
|
|
126
|
-
}
|
|
127
|
-
if (holder.scope === null || holder.settings === null) {
|
|
128
|
-
sendJson(response, 503, { ok: false, error: 'the settings service is not mounted in this deployment' })
|
|
129
|
-
return
|
|
130
|
-
}
|
|
131
|
-
let payload
|
|
132
|
-
try {
|
|
133
|
-
payload = await readJsonBody(request)
|
|
134
|
-
} catch (error) {
|
|
135
|
-
sendJson(response, 400, { ok: false, error: `unreadable request body: ${String(error && error.message ? error.message : error)}` })
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
const invalid = validateOps(payload)
|
|
139
|
-
if (invalid !== null) {
|
|
140
|
-
sendJson(response, 400, { ok: false, error: invalid })
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
const expectedRevision = Number.isInteger(payload.expectedRevision) ? payload.expectedRevision : undefined
|
|
144
|
-
try {
|
|
145
|
-
await holder.scope.mutate(payload.ops, expectedRevision)
|
|
146
|
-
} catch (error) {
|
|
147
|
-
sendJson(response, 409, { ok: false, error: error instanceof Error ? error.message : String(error) })
|
|
148
|
-
return
|
|
149
|
-
}
|
|
150
|
-
const view = viewOf()
|
|
151
|
-
if (view === null) {
|
|
152
|
-
sendJson(response, 503, { ok: false, error: 'settings namespace disappeared after the write' })
|
|
153
|
-
return
|
|
154
|
-
}
|
|
155
|
-
sendJson(response, 200, { ok: true, value: view })
|
|
156
|
-
return
|
|
157
|
-
}
|
|
158
|
-
response.writeHead(405, { allow: 'GET, POST' })
|
|
159
|
-
response.end()
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
ctx.inject(['webServer'], (hctx) => {
|
|
163
|
-
hctx.effect(() => hctx.webServer.register({ kind: 'prefix', path: ROUTE_PREFIX, handler: routeHandler }), 'dsh-github-router: settings routes')
|
|
164
|
-
})
|
|
165
|
-
|
|
166
|
-
return {
|
|
167
|
-
options: () => (holder.scope !== null ? resolveOptions(holder.scope.get()) : resolveOptions(config)),
|
|
168
|
-
}
|
|
169
|
-
}
|