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/dist/client.js CHANGED
@@ -3,169 +3,169 @@ window.__ModuleLoader__.load({
3
3
  factory: (require) => {
4
4
  const module = { exports: {} };
5
5
  const exports = module.exports;
6
- const React = require("react");
7
- const h = React.createElement;
8
- const PREFIX = "harness-supervisor/";
9
-
10
- exports.inject = ["connection", "slots", "sidebarRight", "commandUi", "uiSession"];
11
-
12
- exports.apply = function apply(ctx) {
13
- let disposed = false;
14
- const panelKey = `dsh-main-${crypto.randomUUID()}`;
15
- const viewGenerations = new Map();
16
- const controllers = new Set();
17
- const call = async (method, payload = {}, abort = new AbortController()) => {
18
- if (disposed) throw new Error("HS 插件已关闭");
19
- controllers.add(abort);
20
- try {
21
- const reply = await ctx.connection.rpc.call("/api", PREFIX + method, payload, abort.signal);
22
- if (!reply.ok) throw new Error(reply.error.message);
23
- return reply.value;
24
- } finally { controllers.delete(abort); }
25
- };
26
- const entryUrl = async (sessionId, abort, viewKey) => {
27
- const applicationOrigin = window.location.protocol === "dsh-app:" && window.location.host === "app"
28
- ? "dsh-app://app" : window.location.origin;
29
- const viewGeneration = viewKey ? (viewGenerations.get(viewKey) ?? 0) + 1 : undefined;
30
- if (viewKey) viewGenerations.set(viewKey, viewGeneration);
31
- const entry = await call("open-ui", { ...(sessionId ? { sessionId } : {}), applicationOrigin, ...(viewKey ? { viewKey, viewGeneration } : {}) }, abort);
32
- const url = new URL(entry.url);
33
- if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || url.username || url.password) throw new Error("HS 未返回本机界面地址");
34
- return entry.url;
35
- };
36
- const open = async (sessionId) => {
37
- const url = await entryUrl(sessionId);
38
- if (!disposed) ctx.sidebarRight.openTab("browser", { params: { url } });
39
- };
40
- ctx.effect(() => () => {
41
- disposed = true;
42
- for (const abort of controllers) abort.abort();
43
- controllers.clear();
44
- viewGenerations.clear();
45
- }, "HS UI requests");
46
-
47
- ctx.effect(() => ctx.commandUi.register({
48
- name: "hs", label: () => "Harness Supervisor", description: () => "打开全局 Worker 面板,默认展开当前对话",
49
- available: (session) => typeof session?.sessionId === "string",
50
- ui: { kind: "action", run: (session) => {
51
- // The action consumes the UI token without submitting a model message.
52
- void open(session?.sessionId).catch((error) => window.alert(error.message));
53
- } },
54
- }), "HS open command");
55
-
56
- function SessionView({ sessionId, conversationLayout = false, surface = "sidebar" }) {
57
- const [url, setUrl] = React.useState("");
58
- const [error, setError] = React.useState("");
59
- React.useEffect(() => {
60
- let active = true;
61
- const abort = new AbortController();
62
- setUrl(""); setError("");
63
- void entryUrl(sessionId, abort, `${panelKey}:${surface}`).then(
64
- (next) => { if (active && !disposed) setUrl(next); },
65
- (failure) => { if (active && !disposed) setError(failure.message); },
66
- );
67
- return () => { active = false; abort.abort(); };
68
- }, [sessionId, surface]);
69
- // Follow DSH's full-height Trajectory view contract and leave room for its
70
- // floating composer, whose measured height is supplied by the host.
71
- return h("section", { ...(conversationLayout ? { "data-conversation-composer-overlay": "" } : {}), style: {
72
- display: "flex", flexDirection: "column", height: "100%", minHeight: 0,
73
- boxSizing: "border-box", overflow: "hidden", ...(conversationLayout ? { paddingBottom: "calc(var(--dsh-composer-height, 152px) + 16px)" } : {}),
74
- } },
75
- error ? h("div", { role: "alert", style: { padding: 24, overflowWrap: "anywhere" } }, "无法打开 HS 面板:", error)
76
- : url ? h("iframe", { key: url, src: url, title: "Harness Supervisor · 全局 Worker", sandbox: "allow-scripts allow-same-origin", referrerPolicy: "no-referrer",
77
- style: { flex: 1, width: "100%", minHeight: 0, border: 0 } })
78
- : h("p", { role: "status", style: { padding: 24 } }, "正在连接全局 Worker 面板…"));
79
- }
80
- // A keyed boundary resets the preferred conversation before a new frame mounts.
81
- function WorkerView({ sessionId }) { return h(SessionView, { key: sessionId ?? "global", sessionId, conversationLayout: true, surface: "conversation" }); }
82
- ctx.effect(() => ctx.slots.inject("conversation.view", () => ctx.slots.register({
83
- name: "conversation.view", id: "harness-supervisor", order: 30, label: () => "Harness Supervisor",
84
- }, WorkerView)), "HS conversation view");
85
-
86
- function Introduction() {
87
- return h("header", null,
88
- h("h2", null, "Harness Supervisor"),
89
- h("p", null, "本地、轻量、运行时无关的 Agent Worker 生命周期与通信层。让 DSH 委派和管理 Pi、Qoder、MiniMax Code 等原生 Worker。"),
90
- h("p", { style: { display: "flex", gap: 16 } },
91
- h("a", { href: "https://gitcode.com/chenpi_1/harness-supervisor", target: "_blank", rel: "noopener noreferrer" }, "项目仓库"),
92
- h("a", { href: "https://gitcode.com/chenpi_1/harness-supervisor/blob/main/README.md", target: "_blank", rel: "noopener noreferrer" }, "完整 README")));
93
- }
94
- function MainView() {
95
- const binding = React.useSyncExternalStore(ctx.uiSession.current.subscribe, ctx.uiSession.current.getSnapshot);
96
- // The actual current session determines only the initially expanded group.
97
- return h(SessionView, { key: binding.key ?? "global", sessionId: binding.key });
98
- }
99
- function SidebarIcon({ size = 16 }) {
100
- return h("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
101
- h("path", { d: "M3 5v14M9 5v14M3 12h6M21 7c-1-1-2-2-4-2s-4 1-4 3.5S15 12 17 12s4 1 4 3.5-2 3.5-4 3.5-3-1-4-2" }));
102
- }
103
- ctx.effect(() => ctx.slots.inject("main", () => ctx.slots.register({
104
- name: "main", key: "harness-supervisor",
105
- }, MainView)), "HS main panel");
106
- ctx.effect(() => ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
107
- name: "sidebar.panellist", id: "harness-supervisor", order: 20, label: () => "Harness Supervisor",
108
- }, SidebarIcon)), "HS sidebar entry");
109
-
110
- function Settings() {
111
- const [data, setData] = React.useState();
112
- const [error, setError] = React.useState("");
113
- const [notice, setNotice] = React.useState("");
114
- const [busy, setBusy] = React.useState(false);
115
- const binding = React.useSyncExternalStore(ctx.uiSession.current.subscribe, ctx.uiSession.current.getSnapshot);
116
- const sessionId = binding.key;
117
- const active = React.useRef(true);
118
- React.useEffect(() => {
119
- active.current = true;
120
- return () => { active.current = false; };
121
- }, []);
122
- const run = async (work) => {
123
- if (busy) return;
124
- setBusy(true); setError(""); setNotice("");
125
- try { await work(); }
126
- catch (failure) { if (active.current) setError(failure.message); }
127
- finally { if (active.current) setBusy(false); }
128
- };
129
- const refresh = async () => { const next = await call("status"); if (active.current) setData(next); };
130
- React.useEffect(() => { void run(refresh); }, []);
131
- const intent = data?.update?.intent;
132
- const draining = intent?.phase === "draining";
133
- const phaseText = {
134
- draining: "正在等待已有任务结束", switching: "正在切换配套核心", verifying: "正在验证新核心",
135
- complete: "配套核心已应用", cancelled: "此次更新已取消", failed: "更新未完成,请查看诊断详情",
136
- recovery_required: "需要人工恢复,请查看诊断详情",
137
- }[intent?.phase] || (data?.update?.managed ? "核心已由 HS 托管" : "当前实例尚未托管");
138
- const remaining = data?.update?.drain?.workerIds?.length || 0;
139
- const button = (label, onClick, disabled = false) => h("button", {
140
- type: "button", disabled: busy || disabled, onClick: () => void run(onClick),
141
- style: { border: "1px solid var(--dsw-alias-border-l2, #8886)", borderRadius: 7, padding: "7px 12px", color: "inherit", background: "var(--dsw-alias-bg-layer-1, transparent)", cursor: "pointer" },
142
- }, label);
143
- return h("section", { style: { padding: 20, width: "100%", lineHeight: 1.6 } },
144
- h("div", { style: { maxWidth: 780 } }, h(Introduction),
145
- h("p", null, "人工面板可查看全部 Worker,默认展开当前对话分组;没有当前对话时也可查看。")),
146
- h("div", { style: { width: "100%", height: "70vh", minHeight: 520, margin: "20px 0 28px" } },
147
- h(SessionView, { key: sessionId ?? "global", sessionId, surface: "settings" })),
148
- h("section", { style: { maxWidth: 780 } },
149
- h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
150
- button("在右侧栏打开全局面板", () => open(sessionId), !sessionId),
151
- button("刷新状态", refresh)),
152
- h("h3", null, "配套 HS 核心"),
153
- h("p", null, `插件 ${data?.pluginVersion || "0.1.1"} · 配套核心 ${data?.pairedCoreVersion || "0.10.1"} · 运行核心 ${data?.supervisor?.version || "正在读取"}`),
154
- h("p", null, "等待已有任务结束后切换,期间仍可审批和取消任务。"),
155
- h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
156
- button("应用配套 HS 核心", async () => { const result = await call("apply-core"); if (active.current) setNotice(result.alreadyActive ? "当前已是配套 HS 核心。" : "已登记更新;排空完成后由 HS 自动切换。"); await refresh(); }, draining || ["switching", "verifying"].includes(intent?.phase)),
157
- draining && button("取消此次更新", async () => { await call("cancel-update", { intentId: intent.id }); await refresh(); if (active.current) setNotice("已取消此次更新。"); })),
158
- notice && h("p", { role: "status" }, notice),
159
- error && h("p", { role: "alert", style: { color: "var(--dsw-alias-state-error-primary, #c33)", overflowWrap: "anywhere" } }, error),
160
- data?.update && h("p", { role: "status" }, phaseText, draining ? `,剩余 ${remaining} 个 Worker。` : "。"),
161
- data && h("details", null, h("summary", null, "诊断详情"), h("pre", { style: { whiteSpace: "pre-wrap", overflowWrap: "anywhere", fontSize: 12 } }, JSON.stringify({ update: data.update, sessions: data.sessions }, null, 2))),
162
- h("p", { style: { fontSize: 12, opacity: 0.75 } }, "顶部标签、左侧导航、设置页和 /hs 均为人工全局视图,默认展开当前对话分组。核心重启后重新打开面板。")));
163
- }
164
- ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
165
- name: "settings.section", id: "harness-supervisor", order: 80,
166
- label: () => "Harness Supervisor",
167
- }, Settings)), "HS settings section");
168
- };
6
+ const React = require("react");
7
+ const h = React.createElement;
8
+ const PREFIX = "harness-supervisor/";
9
+
10
+ exports.inject = ["connection", "slots", "sidebarRight", "commandUi", "uiSession"];
11
+
12
+ exports.apply = function apply(ctx) {
13
+ let disposed = false;
14
+ const panelKey = `dsh-main-${crypto.randomUUID()}`;
15
+ const viewGenerations = new Map();
16
+ const controllers = new Set();
17
+ const call = async (method, payload = {}, abort = new AbortController()) => {
18
+ if (disposed) throw new Error("HS 插件已关闭");
19
+ controllers.add(abort);
20
+ try {
21
+ const reply = await ctx.connection.rpc.call("/api", PREFIX + method, payload, abort.signal);
22
+ if (!reply.ok) throw new Error(reply.error.message);
23
+ return reply.value;
24
+ } finally { controllers.delete(abort); }
25
+ };
26
+ const entryUrl = async (sessionId, abort, viewKey) => {
27
+ const applicationOrigin = window.location.protocol === "dsh-app:" && window.location.host === "app"
28
+ ? "dsh-app://app" : window.location.origin;
29
+ const viewGeneration = viewKey ? (viewGenerations.get(viewKey) ?? 0) + 1 : undefined;
30
+ if (viewKey) viewGenerations.set(viewKey, viewGeneration);
31
+ const entry = await call("open-ui", { ...(sessionId ? { sessionId } : {}), applicationOrigin, ...(viewKey ? { viewKey, viewGeneration } : {}) }, abort);
32
+ const url = new URL(entry.url);
33
+ if (url.protocol !== "http:" || url.hostname !== "127.0.0.1" || url.username || url.password) throw new Error("HS 未返回本机界面地址");
34
+ return entry.url;
35
+ };
36
+ const open = async (sessionId) => {
37
+ const url = await entryUrl(sessionId);
38
+ if (!disposed) ctx.sidebarRight.openTab("browser", { params: { url } });
39
+ };
40
+ ctx.effect(() => () => {
41
+ disposed = true;
42
+ for (const abort of controllers) abort.abort();
43
+ controllers.clear();
44
+ viewGenerations.clear();
45
+ }, "HS UI requests");
46
+
47
+ ctx.effect(() => ctx.commandUi.register({
48
+ name: "hs", label: () => "Harness Supervisor", description: () => "打开全局 Worker 面板,默认展开当前对话",
49
+ available: (session) => typeof session?.sessionId === "string",
50
+ ui: { kind: "action", run: (session) => {
51
+ // The action consumes the UI token without submitting a model message.
52
+ void open(session?.sessionId).catch((error) => window.alert(error.message));
53
+ } },
54
+ }), "HS open command");
55
+
56
+ function SessionView({ sessionId, conversationLayout = false, surface = "sidebar" }) {
57
+ const [url, setUrl] = React.useState("");
58
+ const [error, setError] = React.useState("");
59
+ React.useEffect(() => {
60
+ let active = true;
61
+ const abort = new AbortController();
62
+ setUrl(""); setError("");
63
+ void entryUrl(sessionId, abort, `${panelKey}:${surface}`).then(
64
+ (next) => { if (active && !disposed) setUrl(next); },
65
+ (failure) => { if (active && !disposed) setError(failure.message); },
66
+ );
67
+ return () => { active = false; abort.abort(); };
68
+ }, [sessionId, surface]);
69
+ // Follow DSH's full-height Trajectory view contract and leave room for its
70
+ // floating composer, whose measured height is supplied by the host.
71
+ return h("section", { ...(conversationLayout ? { "data-conversation-composer-overlay": "" } : {}), style: {
72
+ display: "flex", flexDirection: "column", height: "100%", minHeight: 0,
73
+ boxSizing: "border-box", overflow: "hidden", ...(conversationLayout ? { paddingBottom: "calc(var(--dsh-composer-height, 152px) + 16px)" } : {}),
74
+ } },
75
+ error ? h("div", { role: "alert", style: { padding: 24, overflowWrap: "anywhere" } }, "无法打开 HS 面板:", error)
76
+ : url ? h("iframe", { key: url, src: url, title: "Harness Supervisor · 全局 Worker", sandbox: "allow-scripts allow-same-origin", referrerPolicy: "no-referrer",
77
+ style: { flex: 1, width: "100%", minHeight: 0, border: 0 } })
78
+ : h("p", { role: "status", style: { padding: 24 } }, "正在连接全局 Worker 面板…"));
79
+ }
80
+ // A keyed boundary resets the preferred conversation before a new frame mounts.
81
+ function WorkerView({ sessionId }) { return h(SessionView, { key: sessionId ?? "global", sessionId, conversationLayout: true, surface: "conversation" }); }
82
+ ctx.effect(() => ctx.slots.inject("conversation.view", () => ctx.slots.register({
83
+ name: "conversation.view", id: "harness-supervisor", order: 30, label: () => "Harness Supervisor",
84
+ }, WorkerView)), "HS conversation view");
85
+
86
+ function Introduction() {
87
+ return h("header", null,
88
+ h("h2", null, "Harness Supervisor"),
89
+ h("p", null, "本地、轻量、运行时无关的 Agent Worker 生命周期与通信层。让 DSH 委派和管理 Pi、Qoder、MiniMax Code 等原生 Worker。"),
90
+ h("p", { style: { display: "flex", gap: 16 } },
91
+ h("a", { href: "https://gitcode.com/chenpi_1/harness-supervisor", target: "_blank", rel: "noopener noreferrer" }, "项目仓库"),
92
+ h("a", { href: "https://gitcode.com/chenpi_1/harness-supervisor/blob/main/README.md", target: "_blank", rel: "noopener noreferrer" }, "完整 README")));
93
+ }
94
+ function MainView() {
95
+ const binding = React.useSyncExternalStore(ctx.uiSession.current.subscribe, ctx.uiSession.current.getSnapshot);
96
+ // The actual current session determines only the initially expanded group.
97
+ return h(SessionView, { key: binding.key ?? "global", sessionId: binding.key });
98
+ }
99
+ function SidebarIcon({ size = 16 }) {
100
+ return h("svg", { width: size, height: size, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true },
101
+ h("path", { d: "M3 5v14M9 5v14M3 12h6M21 7c-1-1-2-2-4-2s-4 1-4 3.5S15 12 17 12s4 1 4 3.5-2 3.5-4 3.5-3-1-4-2" }));
102
+ }
103
+ ctx.effect(() => ctx.slots.inject("main", () => ctx.slots.register({
104
+ name: "main", key: "harness-supervisor",
105
+ }, MainView)), "HS main panel");
106
+ ctx.effect(() => ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
107
+ name: "sidebar.panellist", id: "harness-supervisor", order: 20, label: () => "Harness Supervisor",
108
+ }, SidebarIcon)), "HS sidebar entry");
109
+
110
+ function Settings() {
111
+ const [data, setData] = React.useState();
112
+ const [error, setError] = React.useState("");
113
+ const [notice, setNotice] = React.useState("");
114
+ const [busy, setBusy] = React.useState(false);
115
+ const binding = React.useSyncExternalStore(ctx.uiSession.current.subscribe, ctx.uiSession.current.getSnapshot);
116
+ const sessionId = binding.key;
117
+ const active = React.useRef(true);
118
+ React.useEffect(() => {
119
+ active.current = true;
120
+ return () => { active.current = false; };
121
+ }, []);
122
+ const run = async (work) => {
123
+ if (busy) return;
124
+ setBusy(true); setError(""); setNotice("");
125
+ try { await work(); }
126
+ catch (failure) { if (active.current) setError(failure.message); }
127
+ finally { if (active.current) setBusy(false); }
128
+ };
129
+ const refresh = async () => { const next = await call("status"); if (active.current) setData(next); };
130
+ React.useEffect(() => { void run(refresh); }, []);
131
+ const intent = data?.update?.intent;
132
+ const draining = intent?.phase === "draining";
133
+ const phaseText = {
134
+ draining: "正在等待已有任务结束", switching: "正在切换配套核心", verifying: "正在验证新核心",
135
+ complete: "配套核心已应用", cancelled: "此次更新已取消", failed: "更新未完成,请查看诊断详情",
136
+ recovery_required: "需要人工恢复,请查看诊断详情",
137
+ }[intent?.phase] || (data?.update?.managed ? "核心已由 HS 托管" : "当前实例尚未托管");
138
+ const remaining = data?.update?.drain?.workerIds?.length || 0;
139
+ const button = (label, onClick, disabled = false) => h("button", {
140
+ type: "button", disabled: busy || disabled, onClick: () => void run(onClick),
141
+ style: { border: "1px solid var(--dsw-alias-border-l2, #8886)", borderRadius: 7, padding: "7px 12px", color: "inherit", background: "var(--dsw-alias-bg-layer-1, transparent)", cursor: "pointer" },
142
+ }, label);
143
+ return h("section", { style: { padding: 20, width: "100%", lineHeight: 1.6 } },
144
+ h("div", { style: { maxWidth: 780 } }, h(Introduction),
145
+ h("p", null, "人工面板可查看全部 Worker,默认展开当前对话分组;没有当前对话时也可查看。")),
146
+ h("div", { style: { width: "100%", height: "70vh", minHeight: 520, margin: "20px 0 28px" } },
147
+ h(SessionView, { key: sessionId ?? "global", sessionId, surface: "settings" })),
148
+ h("section", { style: { maxWidth: 780 } },
149
+ h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
150
+ button("在右侧栏打开全局面板", () => open(sessionId), !sessionId),
151
+ button("刷新状态", refresh)),
152
+ h("h3", null, "配套 HS 核心"),
153
+ h("p", null, `插件 ${data?.pluginVersion || "0.1.2"} · 配套核心 ${data?.pairedCoreVersion || "0.10.2"} · 运行核心 ${data?.supervisor?.version || "正在读取"}`),
154
+ h("p", null, "等待已有任务结束后切换,期间仍可审批和取消任务。"),
155
+ h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap" } },
156
+ button("应用配套 HS 核心", async () => { const result = await call("apply-core"); if (active.current) setNotice(result.alreadyActive ? "当前已是配套 HS 核心。" : "已登记更新;排空完成后由 HS 自动切换。"); await refresh(); }, draining || ["switching", "verifying"].includes(intent?.phase)),
157
+ draining && button("取消此次更新", async () => { await call("cancel-update", { intentId: intent.id }); await refresh(); if (active.current) setNotice("已取消此次更新。"); })),
158
+ notice && h("p", { role: "status" }, notice),
159
+ error && h("p", { role: "alert", style: { color: "var(--dsw-alias-state-error-primary, #c33)", overflowWrap: "anywhere" } }, error),
160
+ data?.update && h("p", { role: "status" }, phaseText, draining ? `,剩余 ${remaining} 个 Worker。` : "。"),
161
+ data && h("details", null, h("summary", null, "诊断详情"), h("pre", { style: { whiteSpace: "pre-wrap", overflowWrap: "anywhere", fontSize: 12 } }, JSON.stringify({ update: data.update, sessions: data.sessions }, null, 2))),
162
+ h("p", { style: { fontSize: 12, opacity: 0.75 } }, "顶部标签、左侧导航、设置页和 /hs 均为人工全局视图,默认展开当前对话分组。核心重启后重新打开面板。")));
163
+ }
164
+ ctx.effect(() => ctx.slots.inject("settings.section", () => ctx.slots.register({
165
+ name: "settings.section", id: "harness-supervisor", order: 80,
166
+ label: () => "Harness Supervisor",
167
+ }, Settings)), "HS settings section");
168
+ };
169
169
 
170
170
  return module.exports;
171
171
  }
package/dist/index.js CHANGED
@@ -1,86 +1,86 @@
1
- import path from "node:path";
2
- import os from "node:os";
3
- import { fileURLToPath } from "node:url";
4
- import { randomUUID } from "node:crypto";
5
- import * as mcpClient from "@deepseek-ai/dsh-mcp-client";
6
- import z from "@deepseek-ai/schemastery";
7
- import { SupervisorClient, prepareManagedRuntime } from "harness-supervisor";
8
- import { AgentBridge, createRpcHandler, createFetchRoutes } from "./bridge.js";
9
- import { resolveNodeExecutable } from "./node-runtime.js";
10
-
11
- export const name = "harness-supervisor";
12
- export const inject = ["agents", "connection", "webServer"];
13
- export const Config = z.object({
14
- stateDir: z.string().default(""),
15
- nodeExecutable: z.string().default(""),
16
- });
17
-
18
- const coreRoot = path.resolve(path.dirname(fileURLToPath(import.meta.resolve("harness-supervisor"))), "../..");
19
- const EXPECTED_CORE_VERSION = "0.10.1";
20
-
21
- export function assertCapabilities(info) {
22
- const required = ["ownerScope", "webUi", "managedRuntime", "drain"];
23
- if (!info?.capabilities || required.some((key) => info.capabilities[key] !== true)) {
24
- throw new Error(`The shared HS daemon (${info?.version ?? "unknown"}) lacks required capabilities. Migrate it in a maintenance window; the plugin will not replace it or use global model access.`);
25
- }
26
- }
27
-
28
- export async function apply(ctx, config) {
29
- if (!ctx.agents?.list || !ctx.agents?.get || !ctx.connection?.fetch?.register || typeof ctx.effect !== "function") {
30
- throw new Error("DSH 0.2.2-rc.1 Agent and Connection APIs are required");
31
- }
32
- const stateDir = path.resolve(config.stateDir || process.env.HARNESS_SUPERVISOR_HOME || path.join(os.homedir(), ".harness-supervisor"));
33
- const namespace = path.resolve(process.env.DSH_HOME || path.join(os.homedir(), ".dsh"));
34
- const nodeExecutable = resolveNodeExecutable(config.nodeExecutable);
35
- // Stage before auto-start: plugin removal must not remove a live daemon's code.
36
- // An existing daemon still wins its normal lock and is checked, never replaced.
37
- const bundledRuntime = await prepareManagedRuntime(stateDir, coreRoot);
38
- const client = new SupervisorClient({ stateDir, daemonEntry: path.join(bundledRuntime.root, "dist/src/daemon-entry.js"), nodeExecutable });
39
- const connections = new Map();
40
- const request = async (method, params = {}) => client.request(method, params, { timeoutMs: 120_000 });
41
- ctx.effect(() => () => client.close(), "HS operator IPC connection");
42
- // The daemon's own managed resolver selects the active core. No host install is changed.
43
- const info = await request("info");
44
- assertCapabilities(info);
45
-
46
- const bridge = new AgentBridge({
47
- namespace,
48
- attach: (agent, ownerRef) => agent.ctx.plugin(mcpClient, {
49
- transport: "stdio", serverName: "harness_supervisor", command: nodeExecutable,
50
- args: [path.join(bundledRuntime.root, "dist/src/mcp.js"), "--state-dir", stateDir, "--owner-ref", ownerRef, "--notify-none"],
51
- env: {}, cwd: agent.session.meta?.cwd || process.cwd(), toolCallTimeoutMs: 150_000,
52
- startupTimeoutMs: 30_000, closeTimeoutMs: 5_000, expandTemplates: false, failOnStartupError: true,
53
- }),
54
- report: (agent, status) => {
55
- connections.set(agent, status);
56
- if (status.state === "failed") ctx.logger.warn(`HS session connection failed: ${status.message}`);
57
- },
58
- });
59
- ctx.effect(() => () => bridge.dispose(), "HS session MCP fibers");
60
- ctx.on("agent/created", async ({ agent }) => bridge.attachAgent(agent));
61
- ctx.on("agent/disposed", ({ agent }) => { connections.delete(agent); return bridge.detachAgent(agent); });
62
- let applying;
63
- const applyCore = () => applying ??= (async () => {
64
- const current = await request("runtime_update_status");
65
- if (current.activeRuntimeId === bundledRuntime.id && !["draining", "switching", "verifying"].includes(current.intent?.phase)) {
66
- return { alreadyActive: true, runtimeId: bundledRuntime.id, version: EXPECTED_CORE_VERSION };
67
- }
68
- const intentId = current.intent?.targetRuntimeId === bundledRuntime.id && ["draining", "switching", "verifying"].includes(current.intent.phase)
69
- ? current.intent.id : randomUUID();
70
- return request("runtime_update", { intentId, runtimeId: bundledRuntime.id });
71
- })().finally(() => { applying = undefined; });
72
- const rpc = createRpcHandler({
73
- agents: ctx.agents, namespace, request,
74
- status: async () => ({
75
- pluginVersion: "0.1.1", pairedCoreVersion: EXPECTED_CORE_VERSION,
76
- supervisor: await request("info"), update: await request("runtime_update_status"),
77
- sessions: [...connections].map(([agent, status]) => ({ sessionId: agent.id, ...status })),
78
- }),
79
- applyCore,
80
- cancelUpdate: (intentId) => request("runtime_update_cancel", { intentId }),
81
- });
82
- for (const route of createFetchRoutes(rpc)) {
83
- ctx.effect(() => ctx.connection.fetch.register(route), `HS owner-only ${route.path}`);
84
- }
85
- await Promise.all(ctx.agents.list().map((agent) => bridge.attachAgent(agent)));
86
- }
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ import { fileURLToPath } from "node:url";
4
+ import { randomUUID } from "node:crypto";
5
+ import * as mcpClient from "@deepseek-ai/dsh-mcp-client";
6
+ import z from "@deepseek-ai/schemastery";
7
+ import { SupervisorClient, prepareManagedRuntime } from "harness-supervisor";
8
+ import { AgentBridge, createRpcHandler, createFetchRoutes } from "./bridge.js";
9
+ import { resolveNodeExecutable } from "./node-runtime.js";
10
+
11
+ export const name = "harness-supervisor";
12
+ export const inject = ["agents", "connection", "webServer"];
13
+ export const Config = z.object({
14
+ stateDir: z.string().default(""),
15
+ nodeExecutable: z.string().default(""),
16
+ });
17
+
18
+ const coreRoot = path.resolve(path.dirname(fileURLToPath(import.meta.resolve("harness-supervisor"))), "../..");
19
+ const EXPECTED_CORE_VERSION = "0.10.2";
20
+
21
+ export function assertCapabilities(info) {
22
+ const required = ["ownerScope", "webUi", "managedRuntime", "drain"];
23
+ if (!info?.capabilities || required.some((key) => info.capabilities[key] !== true)) {
24
+ throw new Error(`The shared HS daemon (${info?.version ?? "unknown"}) lacks required capabilities. Migrate it in a maintenance window; the plugin will not replace it or use global model access.`);
25
+ }
26
+ }
27
+
28
+ export async function apply(ctx, config) {
29
+ if (!ctx.agents?.list || !ctx.agents?.get || !ctx.connection?.fetch?.register || typeof ctx.effect !== "function") {
30
+ throw new Error("DSH 0.2.2-rc.1 Agent and Connection APIs are required");
31
+ }
32
+ const stateDir = path.resolve(config.stateDir || process.env.HARNESS_SUPERVISOR_HOME || path.join(os.homedir(), ".harness-supervisor"));
33
+ const namespace = path.resolve(process.env.DSH_HOME || path.join(os.homedir(), ".dsh"));
34
+ const nodeExecutable = resolveNodeExecutable(config.nodeExecutable);
35
+ // Stage before auto-start: plugin removal must not remove a live daemon's code.
36
+ // An existing daemon still wins its normal lock and is checked, never replaced.
37
+ const bundledRuntime = await prepareManagedRuntime(stateDir, coreRoot);
38
+ const client = new SupervisorClient({ stateDir, daemonEntry: path.join(bundledRuntime.root, "dist/src/daemon-entry.js"), nodeExecutable });
39
+ const connections = new Map();
40
+ const request = async (method, params = {}) => client.request(method, params, { timeoutMs: 120_000 });
41
+ ctx.effect(() => () => client.close(), "HS operator IPC connection");
42
+ // The daemon's own managed resolver selects the active core. No host install is changed.
43
+ const info = await request("info");
44
+ assertCapabilities(info);
45
+
46
+ const bridge = new AgentBridge({
47
+ namespace,
48
+ attach: (agent, ownerRef) => agent.ctx.plugin(mcpClient, {
49
+ transport: "stdio", serverName: "harness_supervisor", command: nodeExecutable,
50
+ args: [path.join(bundledRuntime.root, "dist/src/mcp.js"), "--state-dir", stateDir, "--owner-ref", ownerRef, "--notify-none"],
51
+ env: {}, cwd: agent.session.meta?.cwd || process.cwd(), toolCallTimeoutMs: 150_000,
52
+ startupTimeoutMs: 30_000, closeTimeoutMs: 5_000, expandTemplates: false, failOnStartupError: true,
53
+ }),
54
+ report: (agent, status) => {
55
+ connections.set(agent, status);
56
+ if (status.state === "failed") ctx.logger.warn(`HS session connection failed: ${status.message}`);
57
+ },
58
+ });
59
+ ctx.effect(() => () => bridge.dispose(), "HS session MCP fibers");
60
+ ctx.on("agent/created", async ({ agent }) => bridge.attachAgent(agent));
61
+ ctx.on("agent/disposed", ({ agent }) => { connections.delete(agent); return bridge.detachAgent(agent); });
62
+ let applying;
63
+ const applyCore = () => applying ??= (async () => {
64
+ const current = await request("runtime_update_status");
65
+ if (current.activeRuntimeId === bundledRuntime.id && !["draining", "switching", "verifying"].includes(current.intent?.phase)) {
66
+ return { alreadyActive: true, runtimeId: bundledRuntime.id, version: EXPECTED_CORE_VERSION };
67
+ }
68
+ const intentId = current.intent?.targetRuntimeId === bundledRuntime.id && ["draining", "switching", "verifying"].includes(current.intent.phase)
69
+ ? current.intent.id : randomUUID();
70
+ return request("runtime_update", { intentId, runtimeId: bundledRuntime.id });
71
+ })().finally(() => { applying = undefined; });
72
+ const rpc = createRpcHandler({
73
+ agents: ctx.agents, namespace, request,
74
+ status: async () => ({
75
+ pluginVersion: "0.1.2", pairedCoreVersion: EXPECTED_CORE_VERSION,
76
+ supervisor: await request("info"), update: await request("runtime_update_status"),
77
+ sessions: [...connections].map(([agent, status]) => ({ sessionId: agent.id, ...status })),
78
+ }),
79
+ applyCore,
80
+ cancelUpdate: (intentId) => request("runtime_update_cancel", { intentId }),
81
+ });
82
+ for (const route of createFetchRoutes(rpc)) {
83
+ ctx.effect(() => ctx.connection.fetch.register(route), `HS owner-only ${route.path}`);
84
+ }
85
+ await Promise.all(ctx.agents.list().map((agent) => bridge.attachAgent(agent)));
86
+ }
@@ -1,17 +1,17 @@
1
- import { existsSync } from "node:fs";
2
- import path from "node:path";
3
-
4
- /** Desktop's Electron executable needs its own flags; use its shipped Node instead. */
5
- export function resolveNodeExecutable(explicit, runtime = process) {
6
- if (explicit) {
7
- if (!path.isAbsolute(explicit) || !existsSync(explicit) || /\.(?:cmd|bat|ps1)$/i.test(explicit)) {
8
- throw new Error("nodeExecutable must name an existing absolute Node executable, not a shell shim");
9
- }
10
- return explicit;
11
- }
12
- if (!runtime.versions.electron) return runtime.execPath;
13
- const resources = runtime.resourcesPath || path.join(path.dirname(runtime.execPath), "resources");
14
- const shipped = path.join(resources, "runtime/primary-runtime/dependencies/node/bin", runtime.platform === "win32" ? "node.exe" : "node");
15
- if (existsSync(shipped)) return shipped;
16
- throw new Error("The DSH Desktop Node runtime was not found. Configure nodeExecutable with Node >=22.19; HS will not launch Electron as a daemon.");
17
- }
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+
4
+ /** Desktop's Electron executable needs its own flags; use its shipped Node instead. */
5
+ export function resolveNodeExecutable(explicit, runtime = process) {
6
+ if (explicit) {
7
+ if (!path.isAbsolute(explicit) || !existsSync(explicit) || /\.(?:cmd|bat|ps1)$/i.test(explicit)) {
8
+ throw new Error("nodeExecutable must name an existing absolute Node executable, not a shell shim");
9
+ }
10
+ return explicit;
11
+ }
12
+ if (!runtime.versions.electron) return runtime.execPath;
13
+ const resources = runtime.resourcesPath || path.join(path.dirname(runtime.execPath), "resources");
14
+ const shipped = path.join(resources, "runtime/primary-runtime/dependencies/node/bin", runtime.platform === "win32" ? "node.exe" : "node");
15
+ if (existsSync(shipped)) return shipped;
16
+ throw new Error("The DSH Desktop Node runtime was not found. Configure nodeExecutable with Node >=22.19; HS will not launch Electron as a daemon.");
17
+ }
@@ -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.