dsh-harness-supervisor 0.1.1 → 0.1.2
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 -21
- package/README.md +48 -48
- package/cordis.patch.yml +7 -7
- package/dist/bridge.js +157 -157
- package/dist/client.js +163 -163
- package/dist/index.js +86 -86
- package/dist/node-runtime.js +17 -17
- package/node_modules/harness-supervisor/LICENSE +21 -21
- package/node_modules/harness-supervisor/README.md +461 -461
- package/node_modules/harness-supervisor/dist/src/cli.js +88 -88
- package/node_modules/harness-supervisor/dist/src/version.d.ts +1 -1
- package/node_modules/harness-supervisor/dist/src/version.js +1 -1
- package/node_modules/harness-supervisor/dist/src/worker-protocol.js +2 -2
- package/node_modules/harness-supervisor/dist/web/assets/{index-CGX0pkMi.css → index-BqbkyngR.css} +1 -1
- package/node_modules/harness-supervisor/dist/web/assets/{qoder-JStW91Ty.svg → qoder-DMyhGYme.svg} +27 -27
- package/node_modules/harness-supervisor/dist/web/index.html +14 -14
- package/node_modules/harness-supervisor/node_modules/which/bin/node-which +0 -0
- package/node_modules/harness-supervisor/package.json +1 -1
- package/node_modules/harness-supervisor/production-lock.json +173 -173
- package/package.json +3 -3
- /package/node_modules/harness-supervisor/dist/web/assets/{index-CBACx3zJ.js → index-C22kUlUp.js} +0 -0
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Harness Supervisor contributors
|
|
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.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Harness Supervisor contributors
|
|
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,48 +1,48 @@
|
|
|
1
|
-
# DSH Harness Supervisor
|
|
2
|
-
|
|
3
|
-
将 DSH 会话作为 Harness Supervisor 的 Master,委派 Pi、Qoder、MiniMax Code 等原生 Worker。支持 DSH `0.2.2-rc.1`,配套 HS `0.10.
|
|
4
|
-
|
|
5
|
-
[项目仓库](https://gitcode.com/chenpi_1/harness-supervisor) · [完整项目 README](https://gitcode.com/chenpi_1/harness-supervisor/blob/main/README.md)
|
|
6
|
-
|
|
7
|
-
## 安装与使用
|
|
8
|
-
|
|
9
|
-
在 DSH 插件管理器安装 `dsh-harness-supervisor-0.1.
|
|
10
|
-
|
|
11
|
-
从左侧导航、会话顶部或设置中的 **Harness Supervisor** 打开共用 WebUI。人工视图可查看全部 Worker,默认只展开当前对话分组;没有当前对话时也可在左侧导航和设置页查看。也可用 `/hs` 在右侧浏览器打开同样的全局视图。支持 DSH Desktop 和本机 DSH Web;禁用或卸载插件不会停止已有 Worker。
|
|
12
|
-
|
|
13
|
-
## 通过插件更新核心
|
|
14
|
-
|
|
15
|
-
1. 安装新版插件 `.tgz`。此时仅准备配套核心,运行中的 HS 保持原版本。
|
|
16
|
-
2. 在设置中点击 **Harness Supervisor → 应用配套 HS 核心**。
|
|
17
|
-
3. HS 暂停接收新任务,等待已有任务与队列结束,再切换并验证新核心。可点击 **刷新状态** 查看进展。
|
|
18
|
-
|
|
19
|
-
等待期间仍可回答请求、取消 Worker 或取消此次更新,不会到时强杀任务。关闭设置页或禁用插件后,已登记的更新仍由 HS 继续执行。切换后需重新打开 WebUI。
|
|
20
|
-
|
|
21
|
-
更新的是相同状态目录中的共享核心,不会覆盖源码仓库或全局安装文件。当前使用 `.tgz` 分发,尚未提供在线检查新版功能。旧 `0.9.2` 等缺少所需能力的实例须先安排维护窗口、处理活动 Worker 并停止旧 daemon,插件不会自动替换它们。
|
|
22
|
-
|
|
23
|
-
## 配置与边界
|
|
24
|
-
|
|
25
|
-
默认复用独立 HS 的状态目录:`HARNESS_SUPERVISOR_HOME` 或 `~/.harness-supervisor`。可在插件配置中设置 `stateDir`、`nodeExecutable`;不同状态目录属于不同实例,更新不会跨实例生效。
|
|
26
|
-
|
|
27
|
-
插件为每个真实 DSH Agent 复用一个 MCP 连接,固定会话归属和通知策略。模型只能查询和操作所属会话的 Worker 与通知 ACK,不能修改共享通知目标或执行升级。旧 Worker 不会自动归入 DSH 会话。会话范围是路由护栏,不是操作系统隔离。
|
|
28
|
-
|
|
29
|
-
WebUI 的浏览器入口使用一次性链接兑换独立凭证,不接触 IPC Token。刷新和读取结果不会唤醒模型或自动 ACK;daemon 重启后需重新从入口连接。
|
|
30
|
-
|
|
31
|
-
无法确认进程状态时,自动切换会停止,不会强杀进程或并行启动另一核心。恢复线索位于状态目录的 `runtime-update.json`、`runtime-update.log` 和 `runtime-backups/`;不要删除锁强行恢复。
|
|
32
|
-
|
|
33
|
-
## 开发与打包
|
|
34
|
-
|
|
35
|
-
插件与 HS 核心在同一仓库维护。以下命令在插件目录执行,普通安装无需执行:
|
|
36
|
-
|
|
37
|
-
```powershell
|
|
38
|
-
npm test
|
|
39
|
-
npm run build
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
先在仓库根目录运行 `npm run build`、`npm run pack:core`,再在插件目录打包:
|
|
43
|
-
|
|
44
|
-
```powershell
|
|
45
|
-
npm run pack:release -- --core-tarball ../../.artifacts/harness-supervisor-0.10.
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
HS 核心发布物已包含经过根锁文件校验的生产依赖。插件打包只解包该固定压缩包,对照 `production-lock.json` 核对依赖版本,再使用 npm `bundleDependencies` 制作可直接安装的发布物;不重新解析依赖版本,不执行安装脚本。DSH SDK、React 等宿主能力不随插件重复打包。
|
|
1
|
+
# DSH Harness Supervisor
|
|
2
|
+
|
|
3
|
+
将 DSH 会话作为 Harness Supervisor 的 Master,委派 Pi、Qoder、MiniMax Code 等原生 Worker。支持 DSH `0.2.2-rc.1`,配套 HS `0.10.2`;其他 DSH 版本尚需验收。
|
|
4
|
+
|
|
5
|
+
[项目仓库](https://gitcode.com/chenpi_1/harness-supervisor) · [完整项目 README](https://gitcode.com/chenpi_1/harness-supervisor/blob/main/README.md)
|
|
6
|
+
|
|
7
|
+
## 安装与使用
|
|
8
|
+
|
|
9
|
+
在 DSH 插件管理器安装 `dsh-harness-supervisor-0.1.2.tgz` 并启用。安装包包含核心、依赖和共用 WebUI,无需额外安装 HS 或编译。
|
|
10
|
+
|
|
11
|
+
从左侧导航、会话顶部或设置中的 **Harness Supervisor** 打开共用 WebUI。人工视图可查看全部 Worker,默认只展开当前对话分组;没有当前对话时也可在左侧导航和设置页查看。也可用 `/hs` 在右侧浏览器打开同样的全局视图。支持 DSH Desktop 和本机 DSH Web;禁用或卸载插件不会停止已有 Worker。
|
|
12
|
+
|
|
13
|
+
## 通过插件更新核心
|
|
14
|
+
|
|
15
|
+
1. 安装新版插件 `.tgz`。此时仅准备配套核心,运行中的 HS 保持原版本。
|
|
16
|
+
2. 在设置中点击 **Harness Supervisor → 应用配套 HS 核心**。
|
|
17
|
+
3. HS 暂停接收新任务,等待已有任务与队列结束,再切换并验证新核心。可点击 **刷新状态** 查看进展。
|
|
18
|
+
|
|
19
|
+
等待期间仍可回答请求、取消 Worker 或取消此次更新,不会到时强杀任务。关闭设置页或禁用插件后,已登记的更新仍由 HS 继续执行。切换后需重新打开 WebUI。
|
|
20
|
+
|
|
21
|
+
更新的是相同状态目录中的共享核心,不会覆盖源码仓库或全局安装文件。当前使用 `.tgz` 分发,尚未提供在线检查新版功能。旧 `0.9.2` 等缺少所需能力的实例须先安排维护窗口、处理活动 Worker 并停止旧 daemon,插件不会自动替换它们。
|
|
22
|
+
|
|
23
|
+
## 配置与边界
|
|
24
|
+
|
|
25
|
+
默认复用独立 HS 的状态目录:`HARNESS_SUPERVISOR_HOME` 或 `~/.harness-supervisor`。可在插件配置中设置 `stateDir`、`nodeExecutable`;不同状态目录属于不同实例,更新不会跨实例生效。
|
|
26
|
+
|
|
27
|
+
插件为每个真实 DSH Agent 复用一个 MCP 连接,固定会话归属和通知策略。模型只能查询和操作所属会话的 Worker 与通知 ACK,不能修改共享通知目标或执行升级。旧 Worker 不会自动归入 DSH 会话。会话范围是路由护栏,不是操作系统隔离。
|
|
28
|
+
|
|
29
|
+
WebUI 的浏览器入口使用一次性链接兑换独立凭证,不接触 IPC Token。刷新和读取结果不会唤醒模型或自动 ACK;daemon 重启后需重新从入口连接。
|
|
30
|
+
|
|
31
|
+
无法确认进程状态时,自动切换会停止,不会强杀进程或并行启动另一核心。恢复线索位于状态目录的 `runtime-update.json`、`runtime-update.log` 和 `runtime-backups/`;不要删除锁强行恢复。
|
|
32
|
+
|
|
33
|
+
## 开发与打包
|
|
34
|
+
|
|
35
|
+
插件与 HS 核心在同一仓库维护。以下命令在插件目录执行,普通安装无需执行:
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
npm test
|
|
39
|
+
npm run build
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
先在仓库根目录运行 `npm run build`、`npm run pack:core`,再在插件目录打包:
|
|
43
|
+
|
|
44
|
+
```powershell
|
|
45
|
+
npm run pack:release -- --core-tarball ../../.artifacts/harness-supervisor-0.10.2.tgz
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
HS 核心发布物已包含经过根锁文件校验的生产依赖。插件打包只解包该固定压缩包,对照 `production-lock.json` 核对依赖版本,再使用 npm `bundleDependencies` 制作可直接安装的发布物;不重新解析依赖版本,不执行安装脚本。DSH SDK、React 等宿主能力不随插件重复打包。
|
package/cordis.patch.yml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
- insert:
|
|
2
|
-
- id: harness-supervisor
|
|
3
|
-
name: dsh-harness-supervisor
|
|
4
|
-
|
|
5
|
-
# The plugin explicitly provides a browser entry on both Desktop and Web.
|
|
6
|
-
- id: ui-sidebar-browser
|
|
7
|
-
disabled: false
|
|
1
|
+
- insert:
|
|
2
|
+
- id: harness-supervisor
|
|
3
|
+
name: dsh-harness-supervisor
|
|
4
|
+
|
|
5
|
+
# The plugin explicitly provides a browser entry on both Desktop and Web.
|
|
6
|
+
- id: ui-sidebar-browser
|
|
7
|
+
disabled: false
|
package/dist/bridge.js
CHANGED
|
@@ -1,157 +1,157 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
/** Correlation scope only. It is not a same-user or operating-system sandbox. */
|
|
4
|
-
export function ownerRefFor(namespace, sessionId) {
|
|
5
|
-
if (typeof sessionId !== "string" || !sessionId || sessionId.length > 300) {
|
|
6
|
-
throw new Error("DSH did not supply a valid live Agent/Session identity");
|
|
7
|
-
}
|
|
8
|
-
return `dsh:${createHash("sha256").update(namespace).update("\0").update(sessionId).digest("hex")}`;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** Own native MCP fibers by exact Agent object, including in-flight activation. */
|
|
12
|
-
export class AgentBridge {
|
|
13
|
-
#entries = new Map();
|
|
14
|
-
#closing = false;
|
|
15
|
-
constructor({ namespace, attach, report = () => {} }) {
|
|
16
|
-
this.namespace = namespace;
|
|
17
|
-
this.attach = attach;
|
|
18
|
-
this.report = report;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
attachAgent(agent) {
|
|
22
|
-
if (this.#closing) return Promise.resolve();
|
|
23
|
-
const found = this.#entries.get(agent);
|
|
24
|
-
if (found) return found.ready;
|
|
25
|
-
const entry = { removed: false, fiber: undefined, ready: undefined };
|
|
26
|
-
this.#entries.set(agent, entry);
|
|
27
|
-
entry.ready = (async () => {
|
|
28
|
-
try {
|
|
29
|
-
const ownerRef = ownerRefFor(this.namespace, agent.id);
|
|
30
|
-
if (agent.session?.id !== agent.id || typeof agent.ctx?.plugin !== "function") {
|
|
31
|
-
throw new Error("DSH live Agent capabilities are incompatible (expected 0.2.2-rc.1)");
|
|
32
|
-
}
|
|
33
|
-
// attach returns the Fiber synchronously; awaiting the Fiber waits for ready.
|
|
34
|
-
entry.fiber = this.attach(agent, ownerRef);
|
|
35
|
-
if (!entry.fiber || typeof entry.fiber.dispose !== "function") {
|
|
36
|
-
throw new Error("DSH MCP registration did not return a disposable Fiber");
|
|
37
|
-
}
|
|
38
|
-
if (entry.removed || this.#closing) {
|
|
39
|
-
await entry.fiber.dispose();
|
|
40
|
-
return;
|
|
41
|
-
}
|
|
42
|
-
await entry.fiber;
|
|
43
|
-
if (entry.removed || this.#closing) await entry.fiber.dispose();
|
|
44
|
-
else this.report(agent, { state: "connected", ownerRef });
|
|
45
|
-
} catch (error) {
|
|
46
|
-
if (!entry.removed && !this.#closing) {
|
|
47
|
-
this.report(agent, { state: "failed", message: error instanceof Error ? error.message : String(error) });
|
|
48
|
-
}
|
|
49
|
-
// No global fallback: a failed scope must never register global tools.
|
|
50
|
-
await entry.fiber?.dispose();
|
|
51
|
-
}
|
|
52
|
-
})();
|
|
53
|
-
return entry.ready;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async detachAgent(agent) {
|
|
57
|
-
const entry = this.#entries.get(agent);
|
|
58
|
-
if (!entry) return;
|
|
59
|
-
entry.removed = true;
|
|
60
|
-
this.#entries.delete(agent);
|
|
61
|
-
await entry.fiber?.dispose();
|
|
62
|
-
await entry.ready;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async dispose() {
|
|
66
|
-
if (this.#closing) return;
|
|
67
|
-
this.#closing = true;
|
|
68
|
-
const agents = [...this.#entries.keys()];
|
|
69
|
-
await Promise.allSettled(agents.map((agent) => this.detachAgent(agent)));
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export const RPC_PREFIX = "harness-supervisor/";
|
|
74
|
-
export const RPC_ENDPOINTS = new Set(["open-ui", "status", "apply-core", "cancel-update"]);
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Exact Fetch routes are owner-only in DSH Connection before this adapter runs.
|
|
78
|
-
* Reuse the public RPC wire format without claiming the single /api interceptor.
|
|
79
|
-
*/
|
|
80
|
-
export function createFetchRoutes(handler) {
|
|
81
|
-
return [...RPC_ENDPOINTS].map((method) => ({
|
|
82
|
-
path: `/api/${RPC_PREFIX}${method}`, methods: ["POST"], requestBody: "buffered",
|
|
83
|
-
async fetch(request) {
|
|
84
|
-
if (request.headers.get("content-type")?.split(";", 1)[0]?.trim() !== "application/json") return new Response("Expected application/json", { status: 415 });
|
|
85
|
-
let message;
|
|
86
|
-
try { message = await request.json(); } catch { return new Response("Invalid JSON", { status: 400 }); }
|
|
87
|
-
if (!message || typeof message !== "object" || Array.isArray(message)
|
|
88
|
-
|| Object.keys(message).some((key) => !["type", "rpcId", "method", "payload"].includes(key))
|
|
89
|
-
|| message.type !== "client-request" || typeof message.rpcId !== "string" || !message.rpcId || message.rpcId.length > 200
|
|
90
|
-
|| message.method !== `${RPC_PREFIX}${method}`) return new Response("Invalid RPC envelope", { status: 400 });
|
|
91
|
-
const result = await handler(method, message.payload, request.signal, { principal: { kind: "owner" } });
|
|
92
|
-
return Response.json({ type: "server-response", rpcId: message.rpcId, result });
|
|
93
|
-
},
|
|
94
|
-
}));
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function localApplicationOrigin(value) {
|
|
98
|
-
if (typeof value !== "string") throw new Error("A DSH application origin is required");
|
|
99
|
-
// Desktop has an opaque URL.origin. Preserve only its exact registered
|
|
100
|
-
// application origin so the shared UI can be framed in a conversation view.
|
|
101
|
-
if (value === "dsh-app://app") return value;
|
|
102
|
-
const url = new URL(value);
|
|
103
|
-
if (!['http:', 'https:'].includes(url.protocol) || !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)
|
|
104
|
-
|| url.username || url.password || url.origin !== value) {
|
|
105
|
-
throw new Error("HS embedding is available only in a local DSH application");
|
|
106
|
-
}
|
|
107
|
-
return url.origin;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function createRpcHandler({ agents, namespace, request, status, applyCore, cancelUpdate }) {
|
|
111
|
-
return async (endpoint, payload, signal, peer) => {
|
|
112
|
-
try {
|
|
113
|
-
if (peer?.principal?.kind !== "owner") throw new Error("Only the authenticated DSH owner may manage HS");
|
|
114
|
-
if (signal.aborted) throw new Error("Request aborted");
|
|
115
|
-
const method = endpoint;
|
|
116
|
-
if (!RPC_ENDPOINTS.has(method)) throw new Error("Unknown HS endpoint");
|
|
117
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("Invalid HS request");
|
|
118
|
-
let value;
|
|
119
|
-
if (method === "open-ui") {
|
|
120
|
-
if (Object.keys(payload).some((key) => !["sessionId", "applicationOrigin", "viewKey", "viewGeneration"].includes(key))) throw new Error("Unexpected open-ui field");
|
|
121
|
-
if (payload.viewKey !== undefined && (typeof payload.viewKey !== "string" || !payload.viewKey || payload.viewKey.length > 200)) throw new Error("Invalid panel view key");
|
|
122
|
-
if (payload.viewGeneration !== undefined && (!Number.isSafeInteger(payload.viewGeneration) || payload.viewGeneration < 1 || payload.viewKey === undefined)) throw new Error("Invalid panel generation");
|
|
123
|
-
let preferredOwnerRef;
|
|
124
|
-
if (payload.sessionId !== undefined) {
|
|
125
|
-
const agent = typeof payload.sessionId === "string" ? agents.get(payload.sessionId) : undefined;
|
|
126
|
-
if (!agent || agent.session?.id !== agent.id) throw new Error("Open a live DSH session before choosing its default Worker group");
|
|
127
|
-
preferredOwnerRef = ownerRefFor(namespace, agent.id);
|
|
128
|
-
}
|
|
129
|
-
const frameOrigin = localApplicationOrigin(payload.applicationOrigin);
|
|
130
|
-
try {
|
|
131
|
-
// This route is authenticated owner UI, not a model tool. Its access
|
|
132
|
-
// is global; the live session is only a default expansion preference.
|
|
133
|
-
value = await request("ui_start", { ...(preferredOwnerRef === undefined ? {} : { preferredOwnerRef }), frameOrigin, ...(payload.viewKey === undefined ? {} : { viewKey: payload.viewKey }), ...(payload.viewGeneration === undefined ? {} : { viewGeneration: payload.viewGeneration }) });
|
|
134
|
-
} catch (error) {
|
|
135
|
-
if (["BAD_REQUEST", "UNSUPPORTED"].includes(error?.code)) {
|
|
136
|
-
throw new Error("当前运行的 HS 核心尚不支持顶部面板。请在设置 → Harness Supervisor → 应用配套 HS 核心后,重新打开标签页。");
|
|
137
|
-
}
|
|
138
|
-
throw error;
|
|
139
|
-
}
|
|
140
|
-
} else if (method === "status") {
|
|
141
|
-
if (Object.keys(payload).length) throw new Error("Unexpected status field");
|
|
142
|
-
value = await status();
|
|
143
|
-
} else if (method === "apply-core") {
|
|
144
|
-
if (Object.keys(payload).length) throw new Error("The paired core version is fixed by the plugin");
|
|
145
|
-
value = await applyCore();
|
|
146
|
-
} else {
|
|
147
|
-
if (Object.keys(payload).some((key) => key !== "intentId") || typeof payload.intentId !== "string" || !payload.intentId || payload.intentId.length > 200) {
|
|
148
|
-
throw new Error("A current update intentId is required");
|
|
149
|
-
}
|
|
150
|
-
value = await cancelUpdate(payload.intentId);
|
|
151
|
-
}
|
|
152
|
-
return { ok: true, value };
|
|
153
|
-
} catch (error) {
|
|
154
|
-
return { ok: false, error: { code: "harness-supervisor/request-refused", message: error instanceof Error ? error.message : String(error), details: {} } };
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
}
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/** Correlation scope only. It is not a same-user or operating-system sandbox. */
|
|
4
|
+
export function ownerRefFor(namespace, sessionId) {
|
|
5
|
+
if (typeof sessionId !== "string" || !sessionId || sessionId.length > 300) {
|
|
6
|
+
throw new Error("DSH did not supply a valid live Agent/Session identity");
|
|
7
|
+
}
|
|
8
|
+
return `dsh:${createHash("sha256").update(namespace).update("\0").update(sessionId).digest("hex")}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Own native MCP fibers by exact Agent object, including in-flight activation. */
|
|
12
|
+
export class AgentBridge {
|
|
13
|
+
#entries = new Map();
|
|
14
|
+
#closing = false;
|
|
15
|
+
constructor({ namespace, attach, report = () => {} }) {
|
|
16
|
+
this.namespace = namespace;
|
|
17
|
+
this.attach = attach;
|
|
18
|
+
this.report = report;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
attachAgent(agent) {
|
|
22
|
+
if (this.#closing) return Promise.resolve();
|
|
23
|
+
const found = this.#entries.get(agent);
|
|
24
|
+
if (found) return found.ready;
|
|
25
|
+
const entry = { removed: false, fiber: undefined, ready: undefined };
|
|
26
|
+
this.#entries.set(agent, entry);
|
|
27
|
+
entry.ready = (async () => {
|
|
28
|
+
try {
|
|
29
|
+
const ownerRef = ownerRefFor(this.namespace, agent.id);
|
|
30
|
+
if (agent.session?.id !== agent.id || typeof agent.ctx?.plugin !== "function") {
|
|
31
|
+
throw new Error("DSH live Agent capabilities are incompatible (expected 0.2.2-rc.1)");
|
|
32
|
+
}
|
|
33
|
+
// attach returns the Fiber synchronously; awaiting the Fiber waits for ready.
|
|
34
|
+
entry.fiber = this.attach(agent, ownerRef);
|
|
35
|
+
if (!entry.fiber || typeof entry.fiber.dispose !== "function") {
|
|
36
|
+
throw new Error("DSH MCP registration did not return a disposable Fiber");
|
|
37
|
+
}
|
|
38
|
+
if (entry.removed || this.#closing) {
|
|
39
|
+
await entry.fiber.dispose();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await entry.fiber;
|
|
43
|
+
if (entry.removed || this.#closing) await entry.fiber.dispose();
|
|
44
|
+
else this.report(agent, { state: "connected", ownerRef });
|
|
45
|
+
} catch (error) {
|
|
46
|
+
if (!entry.removed && !this.#closing) {
|
|
47
|
+
this.report(agent, { state: "failed", message: error instanceof Error ? error.message : String(error) });
|
|
48
|
+
}
|
|
49
|
+
// No global fallback: a failed scope must never register global tools.
|
|
50
|
+
await entry.fiber?.dispose();
|
|
51
|
+
}
|
|
52
|
+
})();
|
|
53
|
+
return entry.ready;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async detachAgent(agent) {
|
|
57
|
+
const entry = this.#entries.get(agent);
|
|
58
|
+
if (!entry) return;
|
|
59
|
+
entry.removed = true;
|
|
60
|
+
this.#entries.delete(agent);
|
|
61
|
+
await entry.fiber?.dispose();
|
|
62
|
+
await entry.ready;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async dispose() {
|
|
66
|
+
if (this.#closing) return;
|
|
67
|
+
this.#closing = true;
|
|
68
|
+
const agents = [...this.#entries.keys()];
|
|
69
|
+
await Promise.allSettled(agents.map((agent) => this.detachAgent(agent)));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const RPC_PREFIX = "harness-supervisor/";
|
|
74
|
+
export const RPC_ENDPOINTS = new Set(["open-ui", "status", "apply-core", "cancel-update"]);
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Exact Fetch routes are owner-only in DSH Connection before this adapter runs.
|
|
78
|
+
* Reuse the public RPC wire format without claiming the single /api interceptor.
|
|
79
|
+
*/
|
|
80
|
+
export function createFetchRoutes(handler) {
|
|
81
|
+
return [...RPC_ENDPOINTS].map((method) => ({
|
|
82
|
+
path: `/api/${RPC_PREFIX}${method}`, methods: ["POST"], requestBody: "buffered",
|
|
83
|
+
async fetch(request) {
|
|
84
|
+
if (request.headers.get("content-type")?.split(";", 1)[0]?.trim() !== "application/json") return new Response("Expected application/json", { status: 415 });
|
|
85
|
+
let message;
|
|
86
|
+
try { message = await request.json(); } catch { return new Response("Invalid JSON", { status: 400 }); }
|
|
87
|
+
if (!message || typeof message !== "object" || Array.isArray(message)
|
|
88
|
+
|| Object.keys(message).some((key) => !["type", "rpcId", "method", "payload"].includes(key))
|
|
89
|
+
|| message.type !== "client-request" || typeof message.rpcId !== "string" || !message.rpcId || message.rpcId.length > 200
|
|
90
|
+
|| message.method !== `${RPC_PREFIX}${method}`) return new Response("Invalid RPC envelope", { status: 400 });
|
|
91
|
+
const result = await handler(method, message.payload, request.signal, { principal: { kind: "owner" } });
|
|
92
|
+
return Response.json({ type: "server-response", rpcId: message.rpcId, result });
|
|
93
|
+
},
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function localApplicationOrigin(value) {
|
|
98
|
+
if (typeof value !== "string") throw new Error("A DSH application origin is required");
|
|
99
|
+
// Desktop has an opaque URL.origin. Preserve only its exact registered
|
|
100
|
+
// application origin so the shared UI can be framed in a conversation view.
|
|
101
|
+
if (value === "dsh-app://app") return value;
|
|
102
|
+
const url = new URL(value);
|
|
103
|
+
if (!['http:', 'https:'].includes(url.protocol) || !["127.0.0.1", "localhost", "[::1]"].includes(url.hostname)
|
|
104
|
+
|| url.username || url.password || url.origin !== value) {
|
|
105
|
+
throw new Error("HS embedding is available only in a local DSH application");
|
|
106
|
+
}
|
|
107
|
+
return url.origin;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function createRpcHandler({ agents, namespace, request, status, applyCore, cancelUpdate }) {
|
|
111
|
+
return async (endpoint, payload, signal, peer) => {
|
|
112
|
+
try {
|
|
113
|
+
if (peer?.principal?.kind !== "owner") throw new Error("Only the authenticated DSH owner may manage HS");
|
|
114
|
+
if (signal.aborted) throw new Error("Request aborted");
|
|
115
|
+
const method = endpoint;
|
|
116
|
+
if (!RPC_ENDPOINTS.has(method)) throw new Error("Unknown HS endpoint");
|
|
117
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("Invalid HS request");
|
|
118
|
+
let value;
|
|
119
|
+
if (method === "open-ui") {
|
|
120
|
+
if (Object.keys(payload).some((key) => !["sessionId", "applicationOrigin", "viewKey", "viewGeneration"].includes(key))) throw new Error("Unexpected open-ui field");
|
|
121
|
+
if (payload.viewKey !== undefined && (typeof payload.viewKey !== "string" || !payload.viewKey || payload.viewKey.length > 200)) throw new Error("Invalid panel view key");
|
|
122
|
+
if (payload.viewGeneration !== undefined && (!Number.isSafeInteger(payload.viewGeneration) || payload.viewGeneration < 1 || payload.viewKey === undefined)) throw new Error("Invalid panel generation");
|
|
123
|
+
let preferredOwnerRef;
|
|
124
|
+
if (payload.sessionId !== undefined) {
|
|
125
|
+
const agent = typeof payload.sessionId === "string" ? agents.get(payload.sessionId) : undefined;
|
|
126
|
+
if (!agent || agent.session?.id !== agent.id) throw new Error("Open a live DSH session before choosing its default Worker group");
|
|
127
|
+
preferredOwnerRef = ownerRefFor(namespace, agent.id);
|
|
128
|
+
}
|
|
129
|
+
const frameOrigin = localApplicationOrigin(payload.applicationOrigin);
|
|
130
|
+
try {
|
|
131
|
+
// This route is authenticated owner UI, not a model tool. Its access
|
|
132
|
+
// is global; the live session is only a default expansion preference.
|
|
133
|
+
value = await request("ui_start", { ...(preferredOwnerRef === undefined ? {} : { preferredOwnerRef }), frameOrigin, ...(payload.viewKey === undefined ? {} : { viewKey: payload.viewKey }), ...(payload.viewGeneration === undefined ? {} : { viewGeneration: payload.viewGeneration }) });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (["BAD_REQUEST", "UNSUPPORTED"].includes(error?.code)) {
|
|
136
|
+
throw new Error("当前运行的 HS 核心尚不支持顶部面板。请在设置 → Harness Supervisor → 应用配套 HS 核心后,重新打开标签页。");
|
|
137
|
+
}
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
} else if (method === "status") {
|
|
141
|
+
if (Object.keys(payload).length) throw new Error("Unexpected status field");
|
|
142
|
+
value = await status();
|
|
143
|
+
} else if (method === "apply-core") {
|
|
144
|
+
if (Object.keys(payload).length) throw new Error("The paired core version is fixed by the plugin");
|
|
145
|
+
value = await applyCore();
|
|
146
|
+
} else {
|
|
147
|
+
if (Object.keys(payload).some((key) => key !== "intentId") || typeof payload.intentId !== "string" || !payload.intentId || payload.intentId.length > 200) {
|
|
148
|
+
throw new Error("A current update intentId is required");
|
|
149
|
+
}
|
|
150
|
+
value = await cancelUpdate(payload.intentId);
|
|
151
|
+
}
|
|
152
|
+
return { ok: true, value };
|
|
153
|
+
} catch (error) {
|
|
154
|
+
return { ok: false, error: { code: "harness-supervisor/request-refused", message: error instanceof Error ? error.message : String(error), details: {} } };
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|