@iasiv5/dsh-skip-browser-auth 0.3.6 → 0.3.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,7 +38,7 @@ sudo systemctl restart deepseek-harness.service # 按你的部署方式重启
38
38
  - DSH 0.1.5-rc.1 / 0.1.5-rc.2:carrier-neutral-v2
39
39
  - DSH 0.1.7-rc.1 / 0.1.7-rc.2 / 0.2.0-rc.1 / 0.2.0-rc.2:carrier-neutral-v3
40
40
  其它版本或 runtime/connection 混合版本会自动休眠,不要强行处理。
41
- desktop profile(存在 dsh-desktop-host 的环境)会自动休眠,这是设计行为,不要强行处理或改代码绕过。
41
+ desktop 宿主一律自动休眠(多进程树判别:安装槽位/启动器 argv/宿主二进制,0.3.9 起生效),这是设计行为,不要强行处理或改代码绕过;desktop 用户请直接使用 0.3.9+(0.3.6-0.3.8 在 desktop 有已知缺陷,见 docs/COMPATIBILITY.md §7.4)。
42
42
 
43
43
  1. 先执行 dsh --version 告诉我当前 DSH 版本,然后照常安装:
44
44
  dsh plugin --profile web add @iasiv5/dsh-skip-browser-auth
@@ -101,7 +101,7 @@ runtime=0.1.7-rc.2 + connection=0.1.5-rc.2 → dormant(跨代际混合)
101
101
 
102
102
  - 组合期版本探针使用 runtime 本体与官方 connection 包双锚点,并返回 profile id;路径段和 npm flat manifest 两种布局均支持。任何异常都 fail-closed 到 dormant。
103
103
  - `apply()` backstop 再读取两个 manifest,验证 active profile、patch 传入的 profile id、官方 connection sibling 行的 id/name/disabled/fiber 状态;不满足时在创建服务、注册 route、输出 warning 前 fail loud。
104
- - 宿主 profile 门控:探针先解析 @deepseek-ai/dsh-desktop-host——命中即 desktop 环境,整体安静休眠(设计行为,desktop 上 Electron 自带 token,插件无收益);解析异常按 web 继续走版本对判定,任何异常兜底休眠。
104
+ - 宿主 profile 门控(default-deny):激活需要正向 web 证明——宿主二进制为 node/node.exe;desktop 否决双信号 = 宿主 argv 含 dsh-desktop-host(官方 isPackagedDesktopArgv 同款 launcher fact)∨ @deepseek-ai/dsh-desktop-host 可解析。任一不满足即整体安静休眠(desktop 上 Electron 自带 token,插件无收益;0.3.6 的 desktop 判别缺陷已在 0.3.8 修复,见 docs/COMPATIBILITY.md §7)。
105
105
  - `legacy-web-v1` 保留 0.1.2 的 `/api` prefix 与 buffered bridge。
106
106
  - `carrier-neutral-v2` 使用 runtime loader 取得当前 0.1.5 connection module,支持 `requestBodyMode`、exact Fetch route 和 streaming body,并将 generic `connection.rpc.handle()` 绑定到拥有 Web carrier 的 replacement context,避免 0.1.5 的 405 回归。
107
107
  - `carrier-neutral-v3` 在 v2 契约之上复刻 0.1.7 的三处新语义:`/api` 准入改走 `admit()`(OperatorPeer 作为 RPC handler 第 4 参传入)、共享通道请求经 `connection/request` waterfall(监听插件继续可见)、BrowserAuth stub 使用 mount-preserving URL 语义(token 清理 303 `./`、启动 URL 保留 mount)。
@@ -142,6 +142,8 @@ git diff --exit-code cordis.patch.yml
142
142
  - 三个官方 client factory 的 materialization、profile dispatcher 和未知 profile fail-closed
143
143
  - rc.2 dormant 基线、dispose/unload、Host/Origin/Fetch Metadata trust fence
144
144
  - desktop 宿主 fixture:@deepseek-ai/dsh-desktop-host 桩包在场 + 白名单版本对仍整体休眠(官方行活跃、无插件路由、无横幅)
145
+ - desktop 启动器 argv:宿主进程 argv 含 dsh-desktop-host(app.asar 内信号包不可达的 desktop 形态,0.3.6 事故修复)仍整体休眠;web 宿主 argv 不触发
146
+ - web 证明:宿主二进制非 node(如 Electron run-as-node 的改名启动器)一律休眠;node/node.exe 正常激活
145
147
 
146
148
  完整的 GitHub API 审计、profile 数据模型、adapter seam、client 变体和未来新增代际 checklist 见 [docs/COMPATIBILITY.md](./docs/COMPATIBILITY.md);机制详版见 [docs/DESIGN.md](./docs/DESIGN.md)。
147
149
 
package/cordis.patch.yml CHANGED
@@ -7,16 +7,18 @@
7
7
  # 任何门控条件不成立(锚点解析失败、版本漂移、行不存在、名字漂移、
8
8
  # 官方行未禁用),Replacement 一律保持 dormant(fail-closed)。探针为假时,
9
9
  # 即使后续 patch 层把官方行强制 disabled,行绑定内嵌探针仍保持 dormant。
10
- # 宿主 profile 门控(web-only):表达式先解析 @deepseek-ai/dsh-desktop-host/package.json,
11
- # 解析出合法 URL 即 desktop ⇒ 探针为 null(desktop 一律休眠,设计行为);解析异常
12
- # 按包不存在处理(web 继续),兜底仍是完整版本对白名单(fail-closed)。
10
+ # 宿主 profile 门控(web-only,0.3.8 三重判别):desktop 否决 = desktop-host 包可解析
11
+ # ∨ 宿主 argv 含 dsh-desktop-host;web 证明 = 宿主二进制为 node/node.exe。任一
12
+ # desktop 信号命中或 web 证明不成立 ⇒ 探针为 null(desktop 一律休眠,default-deny);
13
+ # 兜底仍是完整版本对白名单(fail-closed)。
13
14
  - id: connection
14
15
  name: '@deepseek-ai/dsh-client-connection'
15
16
  disabled: !!js >-
16
- (() => { try { const profile = ((() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return null; const desktopSpecifier = "@deepseek-ai/dsh-desktop-host" + '/package.json'; let hostProfile; try { const desktopResolved = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier: desktopSpecifier, attributes: {} }) : internal.resolveSync(desktopSpecifier, ctx.baseUrl, {}); const desktopUrl = desktopResolved === null || typeof desktopResolved !== 'object' ? undefined : desktopResolved.url; hostProfile = typeof desktopUrl === 'string' ? 'desktop' : 'web'; } catch (hostError) { hostProfile = 'web'; } const versionOfUrl = (url, patternSource) => { if (typeof url !== 'string') return null; const decoded = decodeURIComponent(url); const segmented = decoded.match(new RegExp(patternSource)); if (segmented !== null) return segmented[1]; if (decoded.slice(0, 5) === 'file:') { const processRef =
17
- globalThis.process; if (processRef === null || typeof processRef !== 'object' || typeof processRef.getBuiltinModule !== 'function') return null; const fsModule = processRef.getBuiltinModule('node:fs'); const urlModule = processRef.getBuiltinModule('node:url'); if (!fsModule || typeof fsModule.readFileSync !== 'function' || !urlModule || typeof urlModule.fileURLToPath !== 'function') return null; const manifest = JSON.parse(fsModule.readFileSync(urlModule.fileURLToPath(url), 'utf8')); return manifest !== null && typeof manifest === 'object' && typeof manifest.version === 'string' ? manifest.version : null; } return null; }; const anchors = [["@deepseek-ai/dsh","[+]dsh@([^/_]+)[_/]"],["@deepseek-ai/dsh-client-connection","[+]dsh-client-connection@([^/_]+)[_/]"]]; const versions = []; for (const [anchorPackage, patternSource] of anchors) { const specifier = anchorPackage + '/package.json'; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier,
18
- attributes: {} }).url : internal.resolveSync(specifier, ctx.baseUrl, {}).url; } catch (gateError) { return null; } let anchorVersion; try { anchorVersion = versionOfUrl(url, patternSource); } catch (gateError) { return null; } if (anchorVersion === null) return null; versions.push(anchorVersion); } const profiles = [{"id":"legacy-web-v1","pairs":[{"runtime":"0.1.2-rc.1","connection":"0.1.2-rc.1"}],"hostProfiles":["web"]},{"id":"carrier-neutral-v2","pairs":[{"runtime":"0.1.5-rc.1","connection":"0.1.5-rc.1"},{"runtime":"0.1.5-rc.2","connection":"0.1.5-rc.2"}],"hostProfiles":["web"]},{"id":"carrier-neutral-v3","pairs":[{"runtime":"0.1.7-rc.1","connection":"0.1.7-rc.1"},{"runtime":"0.1.7-rc.2","connection":"0.1.7-rc.2"},{"runtime":"0.2.0-rc.1","connection":"0.2.0-rc.1"},{"runtime":"0.2.0-rc.2","connection":"0.2.0-rc.2"}],"hostProfiles":["web"]}]; for (const profile of profiles) { if (profile.hostProfiles.includes(hostProfile) && profile.pairs.some((pair) => pair.runtime === versions[0]
19
- && pair.connection === versions[1])) return profile.id; } return null; } catch (gateError) { return null; } })()); return profile !== null && ["legacy-web-v1","carrier-neutral-v2","carrier-neutral-v3"].includes(profile); } catch (gateError) { return false; } })()
17
+ (() => { try { const profile = ((() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return null; const desktopSpecifier = "@deepseek-ai/dsh-desktop-host" + '/package.json'; let hostProfile; try { const desktopResolved = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier: desktopSpecifier, attributes: {} }) : internal.resolveSync(desktopSpecifier, ctx.baseUrl, {}); const desktopUrl = desktopResolved === null || typeof desktopResolved !== 'object' ? undefined : desktopResolved.url; const desktopByPackage = typeof desktopUrl === 'string'; const hostProcess = typeof process !== 'undefined' ? process : null; const desktopByArgv = hostProcess !== null && typeof hostProcess === 'object' && Array.isArray(hostProcess.argv) && hostProcess.argv.some((arg) => typeof arg === 'string' && arg.indexOf("dsh-desktop-host") !== -1); let slot = ''; try { const decodedBase = decodeURIComponent(typeof
18
+ ctx.baseUrl === 'string' ? ctx.baseUrl : ''); const trimmedBase = decodedBase.endsWith('/') ? decodedBase.slice(0, -1) : decodedBase; const slotIndex = trimmedBase.lastIndexOf('/'); slot = trimmedBase.slice(slotIndex + 1).toLowerCase(); } catch (slotError) { slot = ''; } const desktopBySlot = slot === "desktop"; const execPath = hostProcess !== null && typeof hostProcess === 'object' && typeof hostProcess.execPath === 'string' ? hostProcess.execPath : ''; const execBase = execPath.slice(Math.max(execPath.lastIndexOf('/'), execPath.lastIndexOf('\\')) + 1).toLowerCase(); const webByBinary = execBase === 'node' || execBase === 'node.exe' || execBase === 'nodejs' || execBase === 'nodejs.exe'; hostProfile = desktopByPackage || desktopByArgv || desktopBySlot || !webByBinary ? 'desktop' : 'web'; } catch (hostError) { hostProfile = 'web'; } const versionOfUrl = (url, patternSource) => { if (typeof url !== 'string') return null; const decoded = decodeURIComponent(url); const segmented =
19
+ decoded.match(new RegExp(patternSource)); if (segmented !== null) return segmented[1]; if (decoded.slice(0, 5) === 'file:') { const processRef = globalThis.process; if (processRef === null || typeof processRef !== 'object' || typeof processRef.getBuiltinModule !== 'function') return null; const fsModule = processRef.getBuiltinModule('node:fs'); const urlModule = processRef.getBuiltinModule('node:url'); if (!fsModule || typeof fsModule.readFileSync !== 'function' || !urlModule || typeof urlModule.fileURLToPath !== 'function') return null; const manifest = JSON.parse(fsModule.readFileSync(urlModule.fileURLToPath(url), 'utf8')); return manifest !== null && typeof manifest === 'object' && typeof manifest.version === 'string' ? manifest.version : null; } return null; }; const anchors = [["@deepseek-ai/dsh","[+]dsh@([^/_]+)[_/]"],["@deepseek-ai/dsh-client-connection","[+]dsh-client-connection@([^/_]+)[_/]"]]; const versions = []; for (const [anchorPackage, patternSource] of anchors) {
20
+ const specifier = anchorPackage + '/package.json'; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier, attributes: {} }).url : internal.resolveSync(specifier, ctx.baseUrl, {}).url; } catch (gateError) { return null; } let anchorVersion; try { anchorVersion = versionOfUrl(url, patternSource); } catch (gateError) { return null; } if (anchorVersion === null) return null; versions.push(anchorVersion); } const profiles =
21
+ [{"id":"legacy-web-v1","pairs":[{"runtime":"0.1.2-rc.1","connection":"0.1.2-rc.1"}],"hostProfiles":["web"]},{"id":"carrier-neutral-v2","pairs":[{"runtime":"0.1.5-rc.1","connection":"0.1.5-rc.1"},{"runtime":"0.1.5-rc.2","connection":"0.1.5-rc.2"}],"hostProfiles":["web"]},{"id":"carrier-neutral-v3","pairs":[{"runtime":"0.1.7-rc.1","connection":"0.1.7-rc.1"},{"runtime":"0.1.7-rc.2","connection":"0.1.7-rc.2"},{"runtime":"0.2.0-rc.1","connection":"0.2.0-rc.1"},{"runtime":"0.2.0-rc.2","connection":"0.2.0-rc.2"}],"hostProfiles":["web"]}]; for (const profile of profiles) { if (profile.hostProfiles.includes(hostProfile) && profile.pairs.some((pair) => pair.runtime === versions[0] && pair.connection === versions[1])) return profile.id; } return null; } catch (gateError) { return null; } })()); return profile !== null && ["legacy-web-v1","carrier-neutral-v2","carrier-neutral-v3"].includes(profile); } catch (gateError) { return false; } })()
20
22
  - insert:
21
23
  - id: trusted-connection
22
24
  name: '@iasiv5/dsh-skip-browser-auth'
@@ -25,13 +27,15 @@
25
27
  config:
26
28
  trustedHosts: !!js ctx.webRuntime.trustedHosts
27
29
  compatibilityProfile: !!js >-
28
- (() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return null; const desktopSpecifier = "@deepseek-ai/dsh-desktop-host" + '/package.json'; let hostProfile; try { const desktopResolved = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier: desktopSpecifier, attributes: {} }) : internal.resolveSync(desktopSpecifier, ctx.baseUrl, {}); const desktopUrl = desktopResolved === null || typeof desktopResolved !== 'object' ? undefined : desktopResolved.url; hostProfile = typeof desktopUrl === 'string' ? 'desktop' : 'web'; } catch (hostError) { hostProfile = 'web'; } const versionOfUrl = (url, patternSource) => { if (typeof url !== 'string') return null; const decoded = decodeURIComponent(url); const segmented = decoded.match(new RegExp(patternSource)); if (segmented !== null) return segmented[1]; if (decoded.slice(0, 5) === 'file:') { const processRef = globalThis.process; if (processRef
29
- === null || typeof processRef !== 'object' || typeof processRef.getBuiltinModule !== 'function') return null; const fsModule = processRef.getBuiltinModule('node:fs'); const urlModule = processRef.getBuiltinModule('node:url'); if (!fsModule || typeof fsModule.readFileSync !== 'function' || !urlModule || typeof urlModule.fileURLToPath !== 'function') return null; const manifest = JSON.parse(fsModule.readFileSync(urlModule.fileURLToPath(url), 'utf8')); return manifest !== null && typeof manifest === 'object' && typeof manifest.version === 'string' ? manifest.version : null; } return null; }; const anchors = [["@deepseek-ai/dsh","[+]dsh@([^/_]+)[_/]"],["@deepseek-ai/dsh-client-connection","[+]dsh-client-connection@([^/_]+)[_/]"]]; const versions = []; for (const [anchorPackage, patternSource] of anchors) { const specifier = anchorPackage + '/package.json'; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier, attributes: {} }).url :
30
- internal.resolveSync(specifier, ctx.baseUrl, {}).url; } catch (gateError) { return null; } let anchorVersion; try { anchorVersion = versionOfUrl(url, patternSource); } catch (gateError) { return null; } if (anchorVersion === null) return null; versions.push(anchorVersion); } const profiles = [{"id":"legacy-web-v1","pairs":[{"runtime":"0.1.2-rc.1","connection":"0.1.2-rc.1"}],"hostProfiles":["web"]},{"id":"carrier-neutral-v2","pairs":[{"runtime":"0.1.5-rc.1","connection":"0.1.5-rc.1"},{"runtime":"0.1.5-rc.2","connection":"0.1.5-rc.2"}],"hostProfiles":["web"]},{"id":"carrier-neutral-v3","pairs":[{"runtime":"0.1.7-rc.1","connection":"0.1.7-rc.1"},{"runtime":"0.1.7-rc.2","connection":"0.1.7-rc.2"},{"runtime":"0.2.0-rc.1","connection":"0.2.0-rc.1"},{"runtime":"0.2.0-rc.2","connection":"0.2.0-rc.2"}],"hostProfiles":["web"]}]; for (const profile of profiles) { if (profile.hostProfiles.includes(hostProfile) && profile.pairs.some((pair) => pair.runtime === versions[0] &&
31
- pair.connection === versions[1])) return profile.id; } return null; } catch (gateError) { return null; } })()
30
+ (() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return null; const desktopSpecifier = "@deepseek-ai/dsh-desktop-host" + '/package.json'; let hostProfile; try { const desktopResolved = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier: desktopSpecifier, attributes: {} }) : internal.resolveSync(desktopSpecifier, ctx.baseUrl, {}); const desktopUrl = desktopResolved === null || typeof desktopResolved !== 'object' ? undefined : desktopResolved.url; const desktopByPackage = typeof desktopUrl === 'string'; const hostProcess = typeof process !== 'undefined' ? process : null; const desktopByArgv = hostProcess !== null && typeof hostProcess === 'object' && Array.isArray(hostProcess.argv) && hostProcess.argv.some((arg) => typeof arg === 'string' && arg.indexOf("dsh-desktop-host") !== -1); let slot = ''; try { const decodedBase = decodeURIComponent(typeof ctx.baseUrl === 'string' ?
31
+ ctx.baseUrl : ''); const trimmedBase = decodedBase.endsWith('/') ? decodedBase.slice(0, -1) : decodedBase; const slotIndex = trimmedBase.lastIndexOf('/'); slot = trimmedBase.slice(slotIndex + 1).toLowerCase(); } catch (slotError) { slot = ''; } const desktopBySlot = slot === "desktop"; const execPath = hostProcess !== null && typeof hostProcess === 'object' && typeof hostProcess.execPath === 'string' ? hostProcess.execPath : ''; const execBase = execPath.slice(Math.max(execPath.lastIndexOf('/'), execPath.lastIndexOf('\\')) + 1).toLowerCase(); const webByBinary = execBase === 'node' || execBase === 'node.exe' || execBase === 'nodejs' || execBase === 'nodejs.exe'; hostProfile = desktopByPackage || desktopByArgv || desktopBySlot || !webByBinary ? 'desktop' : 'web'; } catch (hostError) { hostProfile = 'web'; } const versionOfUrl = (url, patternSource) => { if (typeof url !== 'string') return null; const decoded = decodeURIComponent(url); const segmented = decoded.match(new
32
+ RegExp(patternSource)); if (segmented !== null) return segmented[1]; if (decoded.slice(0, 5) === 'file:') { const processRef = globalThis.process; if (processRef === null || typeof processRef !== 'object' || typeof processRef.getBuiltinModule !== 'function') return null; const fsModule = processRef.getBuiltinModule('node:fs'); const urlModule = processRef.getBuiltinModule('node:url'); if (!fsModule || typeof fsModule.readFileSync !== 'function' || !urlModule || typeof urlModule.fileURLToPath !== 'function') return null; const manifest = JSON.parse(fsModule.readFileSync(urlModule.fileURLToPath(url), 'utf8')); return manifest !== null && typeof manifest === 'object' && typeof manifest.version === 'string' ? manifest.version : null; } return null; }; const anchors = [["@deepseek-ai/dsh","[+]dsh@([^/_]+)[_/]"],["@deepseek-ai/dsh-client-connection","[+]dsh-client-connection@([^/_]+)[_/]"]]; const versions = []; for (const [anchorPackage, patternSource] of anchors) { const
33
+ specifier = anchorPackage + '/package.json'; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier, attributes: {} }).url : internal.resolveSync(specifier, ctx.baseUrl, {}).url; } catch (gateError) { return null; } let anchorVersion; try { anchorVersion = versionOfUrl(url, patternSource); } catch (gateError) { return null; } if (anchorVersion === null) return null; versions.push(anchorVersion); } const profiles =
34
+ [{"id":"legacy-web-v1","pairs":[{"runtime":"0.1.2-rc.1","connection":"0.1.2-rc.1"}],"hostProfiles":["web"]},{"id":"carrier-neutral-v2","pairs":[{"runtime":"0.1.5-rc.1","connection":"0.1.5-rc.1"},{"runtime":"0.1.5-rc.2","connection":"0.1.5-rc.2"}],"hostProfiles":["web"]},{"id":"carrier-neutral-v3","pairs":[{"runtime":"0.1.7-rc.1","connection":"0.1.7-rc.1"},{"runtime":"0.1.7-rc.2","connection":"0.1.7-rc.2"},{"runtime":"0.2.0-rc.1","connection":"0.2.0-rc.1"},{"runtime":"0.2.0-rc.2","connection":"0.2.0-rc.2"}],"hostProfiles":["web"]}]; for (const profile of profiles) { if (profile.hostProfiles.includes(hostProfile) && profile.pairs.some((pair) => pair.runtime === versions[0] && pair.connection === versions[1])) return profile.id; } return null; } catch (gateError) { return null; } })()
32
35
  disabled: !!js >-
33
- !((() => { try { const self = ctx[Symbol.for('cordis.entry')]; if (!self || !self.parent || !self.parent.tree) return false; const official = self.parent.tree.resolve('connection'); if (!official || official.parent !== self.parent) return false; if (official.options.id !== 'connection' || official.options.name !== '@deepseek-ai/dsh-client-connection') return false; const probeAllowed = ((() => { try { const profile = ((() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return null; const desktopSpecifier = "@deepseek-ai/dsh-desktop-host" + '/package.json'; let hostProfile; try { const desktopResolved = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier: desktopSpecifier, attributes: {} }) : internal.resolveSync(desktopSpecifier, ctx.baseUrl, {}); const desktopUrl = desktopResolved === null || typeof desktopResolved !== 'object' ? undefined : desktopResolved.url; hostProfile =
34
- typeof desktopUrl === 'string' ? 'desktop' : 'web'; } catch (hostError) { hostProfile = 'web'; } const versionOfUrl = (url, patternSource) => { if (typeof url !== 'string') return null; const decoded = decodeURIComponent(url); const segmented = decoded.match(new RegExp(patternSource)); if (segmented !== null) return segmented[1]; if (decoded.slice(0, 5) === 'file:') { const processRef = globalThis.process; if (processRef === null || typeof processRef !== 'object' || typeof processRef.getBuiltinModule !== 'function') return null; const fsModule = processRef.getBuiltinModule('node:fs'); const urlModule = processRef.getBuiltinModule('node:url'); if (!fsModule || typeof fsModule.readFileSync !== 'function' || !urlModule || typeof urlModule.fileURLToPath !== 'function') return null; const manifest = JSON.parse(fsModule.readFileSync(urlModule.fileURLToPath(url), 'utf8')); return manifest !== null && typeof manifest === 'object' && typeof manifest.version === 'string' ?
35
- manifest.version : null; } return null; }; const anchors = [["@deepseek-ai/dsh","[+]dsh@([^/_]+)[_/]"],["@deepseek-ai/dsh-client-connection","[+]dsh-client-connection@([^/_]+)[_/]"]]; const versions = []; for (const [anchorPackage, patternSource] of anchors) { const specifier = anchorPackage + '/package.json'; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier, attributes: {} }).url : internal.resolveSync(specifier, ctx.baseUrl, {}).url; } catch (gateError) { return null; } let anchorVersion; try { anchorVersion = versionOfUrl(url, patternSource); } catch (gateError) { return null; } if (anchorVersion === null) return null; versions.push(anchorVersion); } const profiles =
36
+ !((() => { try { const self = ctx[Symbol.for('cordis.entry')]; if (!self || !self.parent || !self.parent.tree) return false; const official = self.parent.tree.resolve('connection'); if (!official || official.parent !== self.parent) return false; if (official.options.id !== 'connection' || official.options.name !== '@deepseek-ai/dsh-client-connection') return false; const probeAllowed = ((() => { try { const profile = ((() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return null; const desktopSpecifier = "@deepseek-ai/dsh-desktop-host" + '/package.json'; let hostProfile; try { const desktopResolved = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier: desktopSpecifier, attributes: {} }) : internal.resolveSync(desktopSpecifier, ctx.baseUrl, {}); const desktopUrl = desktopResolved === null || typeof desktopResolved !== 'object' ? undefined : desktopResolved.url; const
37
+ desktopByPackage = typeof desktopUrl === 'string'; const hostProcess = typeof process !== 'undefined' ? process : null; const desktopByArgv = hostProcess !== null && typeof hostProcess === 'object' && Array.isArray(hostProcess.argv) && hostProcess.argv.some((arg) => typeof arg === 'string' && arg.indexOf("dsh-desktop-host") !== -1); let slot = ''; try { const decodedBase = decodeURIComponent(typeof ctx.baseUrl === 'string' ? ctx.baseUrl : ''); const trimmedBase = decodedBase.endsWith('/') ? decodedBase.slice(0, -1) : decodedBase; const slotIndex = trimmedBase.lastIndexOf('/'); slot = trimmedBase.slice(slotIndex + 1).toLowerCase(); } catch (slotError) { slot = ''; } const desktopBySlot = slot === "desktop"; const execPath = hostProcess !== null && typeof hostProcess === 'object' && typeof hostProcess.execPath === 'string' ? hostProcess.execPath : ''; const execBase = execPath.slice(Math.max(execPath.lastIndexOf('/'), execPath.lastIndexOf('\\')) + 1).toLowerCase(); const
38
+ webByBinary = execBase === 'node' || execBase === 'node.exe' || execBase === 'nodejs' || execBase === 'nodejs.exe'; hostProfile = desktopByPackage || desktopByArgv || desktopBySlot || !webByBinary ? 'desktop' : 'web'; } catch (hostError) { hostProfile = 'web'; } const versionOfUrl = (url, patternSource) => { if (typeof url !== 'string') return null; const decoded = decodeURIComponent(url); const segmented = decoded.match(new RegExp(patternSource)); if (segmented !== null) return segmented[1]; if (decoded.slice(0, 5) === 'file:') { const processRef = globalThis.process; if (processRef === null || typeof processRef !== 'object' || typeof processRef.getBuiltinModule !== 'function') return null; const fsModule = processRef.getBuiltinModule('node:fs'); const urlModule = processRef.getBuiltinModule('node:url'); if (!fsModule || typeof fsModule.readFileSync !== 'function' || !urlModule || typeof urlModule.fileURLToPath !== 'function') return null; const manifest =
39
+ JSON.parse(fsModule.readFileSync(urlModule.fileURLToPath(url), 'utf8')); return manifest !== null && typeof manifest === 'object' && typeof manifest.version === 'string' ? manifest.version : null; } return null; }; const anchors = [["@deepseek-ai/dsh","[+]dsh@([^/_]+)[_/]"],["@deepseek-ai/dsh-client-connection","[+]dsh-client-connection@([^/_]+)[_/]"]]; const versions = []; for (const [anchorPackage, patternSource] of anchors) { const specifier = anchorPackage + '/package.json'; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier, attributes: {} }).url : internal.resolveSync(specifier, ctx.baseUrl, {}).url; } catch (gateError) { return null; } let anchorVersion; try { anchorVersion = versionOfUrl(url, patternSource); } catch (gateError) { return null; } if (anchorVersion === null) return null; versions.push(anchorVersion); } const profiles =
36
40
  [{"id":"legacy-web-v1","pairs":[{"runtime":"0.1.2-rc.1","connection":"0.1.2-rc.1"}],"hostProfiles":["web"]},{"id":"carrier-neutral-v2","pairs":[{"runtime":"0.1.5-rc.1","connection":"0.1.5-rc.1"},{"runtime":"0.1.5-rc.2","connection":"0.1.5-rc.2"}],"hostProfiles":["web"]},{"id":"carrier-neutral-v3","pairs":[{"runtime":"0.1.7-rc.1","connection":"0.1.7-rc.1"},{"runtime":"0.1.7-rc.2","connection":"0.1.7-rc.2"},{"runtime":"0.2.0-rc.1","connection":"0.2.0-rc.1"},{"runtime":"0.2.0-rc.2","connection":"0.2.0-rc.2"}],"hostProfiles":["web"]}]; for (const profile of profiles) { if (profile.hostProfiles.includes(hostProfile) && profile.pairs.some((pair) => pair.runtime === versions[0] && pair.connection === versions[1])) return profile.id; } return null; } catch (gateError) { return null; } })()); return profile !== null && ["legacy-web-v1","carrier-neutral-v2","carrier-neutral-v3"].includes(profile); } catch (gateError) { return false; } })()); if (probeAllowed !== true) return false;
37
41
  return official.disabled === true; } catch (gateError) { return false; } })())
package/lib/gate.js CHANGED
@@ -33,14 +33,38 @@
33
33
  * 两分支都拿不到精确版本 → dormant。任何异常一律 dormant(组合期绝不
34
34
  * fail loud;最后防线在 apply 内 backstop)。
35
35
  *
36
- * 宿主 profile 门控(2026-10-07,web-only / desktop 休眠):表达式在版本对判定
37
- * 之前先解析 desktop 信号包(DESKTOP_HOST_PACKAGE)——解析出合法 {url: string}
38
- * ⇒ desktop,由白名单表条目的 hostProfiles: ['web'] 条件统一拒绝(休眠,
39
- * 策略单一来源 src/compatibility.ts);抛错/返回空值/url 非字符串 ⇒ 视为包
40
- * 不存在(web 继续,组合测试 fake 与真实 v1 loader 契约即如此);resolver
41
- * 缺失仍走原有 null 分支。检测异常的 fail-closed 兜底是版本对判定本身
42
- * (真异常时双锚点同样解析失败 ⇒ null)。Node 侧镜像见 detectHostProfile;
43
- * 语义与解冻条件记录于 docs/COMPATIBILITY.md §7。
36
+ * 宿主 profile 门控(2026-10-07,web-only / desktop 休眠;2026-10-08 0.3.8 信号修订)。
37
+ * 设计公理(0.3.6 desktop 事故后的 default-deny):**激活需要正向 web 证明,
38
+ * 休眠不需要理由**——伤害不对称已被实证(误激活 desktop ⇒ 宿主不可启动;
39
+ * 误休眠 web ⇒ 回到 token 输入,可一分钟定位)。
40
+ * 表达式在版本对判定之前先做宿主形态判别,desktop 否决两条 + web 证明一条:
41
+ * ① desktop 信号包(DESKTOP_HOST_PACKAGE)自 ctx.baseUrl 可解析且得合法
42
+ * {url: string}——app.asar 内置形态的判别;
43
+ * ② 宿主进程 process.argv 含 `dsh-desktop-host`——desktop 宿主以
44
+ * `[execPath, dsh-desktop-host/lib/index.js, <dsh>, <profileDir>, …]` 启动
45
+ * (Electron under ELECTRON_RUN_AS_NODE),与 DSH 官方 dsh-plugin-manager
46
+ * 的 isPackagedDesktopArgv 同款判别:launcher 事实独立于 profile 布局,
47
+ * 不可被依赖抬升污染;
48
+ * ②′ desktop 槽位判别:ctx.baseUrl 目录基名 === 'desktop'——desktop 客户端
49
+ * 恒以 profiles/desktop 槽位启动 loader(官方 desktopProfileDirFromArgv 按
50
+ * profiles 树定位),构造性事实、与进程树形状无关。0.3.8 的事态(宿主以
51
+ * 纯 node 子进程跑 loader,argv/execPath 双双落空)由本信号兜底;
52
+ * ③ web 正向证明:宿主二进制 process.execPath 基名(大小写归一)∈
53
+ * {node, node.exe, nodejs, nodejs.exe}——Windows/macOS/Linux 的 web 全形态
54
+ * (systemd/docker/nvm/fnm/volta/mise/直接 node)经解释器 exec 后 execPath
55
+ * 恒为 node 系二进制;desktop 宿主(Electron run-as-node)恒非 node 系。
56
+ * 非 node 系二进制 ⇒ 非 web ⇒ 休眠(unknown-variant 兜底)。
57
+ * 三者关系:任一 desktop 信号命中,或 web 证明不成立 ⇒ desktop(休眠)。
58
+ * 判 desktop ⇒ 由白名单表条目的 hostProfiles: ['web'] 条件统一拒绝(休眠,
59
+ * 策略单一来源 src/compatibility.ts);全否 ⇒ web 继续;resolver 缺失仍走
60
+ * 原有 null 分支。检测异常的 fail-closed 兜底是版本对判定本身(真异常时
61
+ * 双锚点同样解析失败 ⇒ null)。
62
+ * 信号修订缘由(2026-10-07 desktop 事故):desktop profile 为 hoisted 物化布局,
63
+ * dsh-desktop-host 只在 app.asar 内、自 ctx.baseUrl 不可达 ⇒ 0.3.6 仅凭信号①
64
+ * 误判 web ⇒ 版本对命中 v3 ⇒ Replacement 在 desktop 激活 ⇒ "Desktop Host
65
+ * authentication failed" 崩溃循环(0.3.6 已从 desktop 撤出;事故记录
66
+ * docs/COMPATIBILITY.md §7.4/§7.6)。Node 侧镜像见 detectHostProfile;
67
+ * 语义、事故记录与解冻条件见 docs/COMPATIBILITY.md §7。
44
68
  */
45
69
  import { ACTIVE_COMPATIBILITY_PROFILE_IDS, GATE_PROFILE_TABLE, } from './compatibility.js';
46
70
  export { ACTIVE_COMPATIBILITY_PROFILE_IDS, COMPATIBILITY_PROFILES, GATE_VERSION, GATE_VERSIONS, isActiveCompatibilityProfileId, resolveActiveCompatibilityProfile, resolveCompatibilityProfile, } from './compatibility.js';
@@ -49,8 +73,12 @@ export const CONNECTION_PACKAGE = '@deepseek-ai/dsh-client-connection';
49
73
  export const GATE_RUNTIME_PACKAGE = '@deepseek-ai/dsh';
50
74
  /** 探针锚点(顺序即求值顺序):宿主 runtime 本体在前,被替换对象在后。 */
51
75
  export const GATE_ANCHOR_PACKAGES = [GATE_RUNTIME_PACKAGE, CONNECTION_PACKAGE];
52
- /** desktop 宿主信号包:仅 desktop runtime 携带;解析出合法 URL 即判定 desktop(web-only 门控)。 */
76
+ /** desktop 宿主信号包:仅在 app.asar 内置形态存在(profile 布局下常不可达,见 0.3.8 事故记录);可解析即判定 desktop。 */
53
77
  export const DESKTOP_HOST_PACKAGE = '@deepseek-ai/dsh-desktop-host';
78
+ /** desktop 宿主 argv 判别子串:desktop 宿主入口脚本为 `dsh-desktop-host/lib/index.js`(官方 isPackagedDesktopArgv 同款)。 */
79
+ export const DESKTOP_HOST_ARGV_MARKER = 'dsh-desktop-host';
80
+ /** desktop 安装槽位名:desktop 客户端恒以 profiles/desktop 槽位启动 loader(官方 desktopProfileDirFromArgv 契约)。 */
81
+ export const DESKTOP_HOST_SLOT_NAME = 'desktop';
54
82
  /**
55
83
  * 从已解析 URL 提取 pnpm store 版本段的正则源(`+name@VER_` / `+name@VER/`)。
56
84
  * 探针表达式内嵌的正则由此单一实现生成;仅匹配 pnpm 布局,非 pnpm 布局
@@ -66,7 +94,7 @@ const GATE_ANCHOR_TABLE = GATE_ANCHOR_PACKAGES.map((pkg) => [pkg, versionSegment
66
94
  // 逐锚点解析 `锚点包/package.json`,返回完整 runtime/connection pair 对应的
67
95
  // profile id;candidate profile 也可被识别,是否允许激活由下方 boolean probe
68
96
  // 根据 profile status 决定。解析失败、未知 pair 或混合 pair 一律返回 null。
69
- export const GATE_PROFILE_EXPRESSION = `(() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return null; const desktopSpecifier = ${JSON.stringify(DESKTOP_HOST_PACKAGE)} + '/package.json'; let hostProfile; try { const desktopResolved = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier: desktopSpecifier, attributes: {} }) : internal.resolveSync(desktopSpecifier, ctx.baseUrl, {}); const desktopUrl = desktopResolved === null || typeof desktopResolved !== 'object' ? undefined : desktopResolved.url; hostProfile = typeof desktopUrl === 'string' ? 'desktop' : 'web'; } catch (hostError) { hostProfile = 'web'; } const versionOfUrl = (url, patternSource) => { if (typeof url !== 'string') return null; const decoded = decodeURIComponent(url); const segmented = decoded.match(new RegExp(patternSource)); if (segmented !== null) return segmented[1]; if (decoded.slice(0, 5) === 'file:') { const processRef = globalThis.process; if (processRef === null || typeof processRef !== 'object' || typeof processRef.getBuiltinModule !== 'function') return null; const fsModule = processRef.getBuiltinModule('node:fs'); const urlModule = processRef.getBuiltinModule('node:url'); if (!fsModule || typeof fsModule.readFileSync !== 'function' || !urlModule || typeof urlModule.fileURLToPath !== 'function') return null; const manifest = JSON.parse(fsModule.readFileSync(urlModule.fileURLToPath(url), 'utf8')); return manifest !== null && typeof manifest === 'object' && typeof manifest.version === 'string' ? manifest.version : null; } return null; }; const anchors = ${JSON.stringify(GATE_ANCHOR_TABLE)}; const versions = []; for (const [anchorPackage, patternSource] of anchors) { const specifier = anchorPackage + '/package.json'; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier, attributes: {} }).url : internal.resolveSync(specifier, ctx.baseUrl, {}).url; } catch (gateError) { return null; } let anchorVersion; try { anchorVersion = versionOfUrl(url, patternSource); } catch (gateError) { return null; } if (anchorVersion === null) return null; versions.push(anchorVersion); } const profiles = ${JSON.stringify(GATE_PROFILE_TABLE)}; for (const profile of profiles) { if (profile.hostProfiles.includes(hostProfile) && profile.pairs.some((pair) => pair.runtime === versions[0] && pair.connection === versions[1])) return profile.id; } return null; } catch (gateError) { return null; } })()`;
97
+ export const GATE_PROFILE_EXPRESSION = `(() => { try { const internal = ctx.loader && ctx.loader.internal; if (!internal || typeof internal.resolveSync !== 'function') return null; const desktopSpecifier = ${JSON.stringify(DESKTOP_HOST_PACKAGE)} + '/package.json'; let hostProfile; try { const desktopResolved = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier: desktopSpecifier, attributes: {} }) : internal.resolveSync(desktopSpecifier, ctx.baseUrl, {}); const desktopUrl = desktopResolved === null || typeof desktopResolved !== 'object' ? undefined : desktopResolved.url; const desktopByPackage = typeof desktopUrl === 'string'; const hostProcess = typeof process !== 'undefined' ? process : null; const desktopByArgv = hostProcess !== null && typeof hostProcess === 'object' && Array.isArray(hostProcess.argv) && hostProcess.argv.some((arg) => typeof arg === 'string' && arg.indexOf(${JSON.stringify(DESKTOP_HOST_ARGV_MARKER)}) !== -1); let slot = ''; try { const decodedBase = decodeURIComponent(typeof ctx.baseUrl === 'string' ? ctx.baseUrl : ''); const trimmedBase = decodedBase.endsWith('/') ? decodedBase.slice(0, -1) : decodedBase; const slotIndex = trimmedBase.lastIndexOf('/'); slot = trimmedBase.slice(slotIndex + 1).toLowerCase(); } catch (slotError) { slot = ''; } const desktopBySlot = slot === ${JSON.stringify(DESKTOP_HOST_SLOT_NAME)}; const execPath = hostProcess !== null && typeof hostProcess === 'object' && typeof hostProcess.execPath === 'string' ? hostProcess.execPath : ''; const execBase = execPath.slice(Math.max(execPath.lastIndexOf('/'), execPath.lastIndexOf('\\\\')) + 1).toLowerCase(); const webByBinary = execBase === 'node' || execBase === 'node.exe' || execBase === 'nodejs' || execBase === 'nodejs.exe'; hostProfile = desktopByPackage || desktopByArgv || desktopBySlot || !webByBinary ? 'desktop' : 'web'; } catch (hostError) { hostProfile = 'web'; } const versionOfUrl = (url, patternSource) => { if (typeof url !== 'string') return null; const decoded = decodeURIComponent(url); const segmented = decoded.match(new RegExp(patternSource)); if (segmented !== null) return segmented[1]; if (decoded.slice(0, 5) === 'file:') { const processRef = globalThis.process; if (processRef === null || typeof processRef !== 'object' || typeof processRef.getBuiltinModule !== 'function') return null; const fsModule = processRef.getBuiltinModule('node:fs'); const urlModule = processRef.getBuiltinModule('node:url'); if (!fsModule || typeof fsModule.readFileSync !== 'function' || !urlModule || typeof urlModule.fileURLToPath !== 'function') return null; const manifest = JSON.parse(fsModule.readFileSync(urlModule.fileURLToPath(url), 'utf8')); return manifest !== null && typeof manifest === 'object' && typeof manifest.version === 'string' ? manifest.version : null; } return null; }; const anchors = ${JSON.stringify(GATE_ANCHOR_TABLE)}; const versions = []; for (const [anchorPackage, patternSource] of anchors) { const specifier = anchorPackage + '/package.json'; let url; try { url = internal.version === 'v2' ? internal.resolveSync(ctx.baseUrl, { specifier, attributes: {} }).url : internal.resolveSync(specifier, ctx.baseUrl, {}).url; } catch (gateError) { return null; } let anchorVersion; try { anchorVersion = versionOfUrl(url, patternSource); } catch (gateError) { return null; } if (anchorVersion === null) return null; versions.push(anchorVersion); } const profiles = ${JSON.stringify(GATE_PROFILE_TABLE)}; for (const profile of profiles) { if (profile.hostProfiles.includes(hostProfile) && profile.pairs.some((pair) => pair.runtime === versions[0] && pair.connection === versions[1])) return profile.id; } return null; } catch (gateError) { return null; } })()`;
70
98
  // Boolean activation probe: only active profiles disable the official row.
71
99
  // A known candidate profile remains dormant until its host/client adapter is
72
100
  // promoted, while the profile expression still exposes its exact pair id.
@@ -86,8 +114,10 @@ export function evaluateGate(ctx) {
86
114
  }
87
115
  /**
88
116
  * Node 侧宿主形态检测(apply backstop 镜像):与 GATE_PROFILE_EXPRESSION 内联检测
89
- * 同语义——解析 desktop 信号包得到合法 {url: string} ⇒ 'desktop';抛错/空值/url
90
- * 非字符串 ⇒ 视为包不存在 ⇒ 'web'(fail-closed 兜底是后续版本对判定)。
117
+ * 同语义,desktop 双信号取或——① desktop 信号包自 baseUrl 解析出合法
118
+ * {url: string}(抛错/空值/url 非字符串视为不可达,继续②);② 宿主进程
119
+ * process.argv 含 `dsh-desktop-host`(与官方 isPackagedDesktopArgv 同款)。
120
+ * 两信号皆否 ⇒ 'web'(fail-closed 兜底是后续版本对判定)。
91
121
  * 调用方需已确认 resolver 存在(assertGateBackstop 的 presence 检查先于此执行)。
92
122
  */
93
123
  export function detectHostProfile(internal, baseUrl) {
@@ -97,9 +127,33 @@ export function detectHostProfile(internal, baseUrl) {
97
127
  ? internal.resolveSync(baseUrl, { specifier, attributes: {} })
98
128
  : internal.resolveSync(specifier, baseUrl, {});
99
129
  const url = resolved?.url;
100
- return typeof url === 'string' ? 'desktop' : 'web';
130
+ if (typeof url === 'string')
131
+ return 'desktop';
101
132
  }
102
133
  catch {
103
- return 'web';
134
+ // 信号包不可达/解析异常 ⇒ 落到宿主进程 argv 判别
104
135
  }
136
+ let slot = '';
137
+ try {
138
+ const trimmedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
139
+ const slotIndex = Math.max(trimmedBase.lastIndexOf('/'), trimmedBase.lastIndexOf('\\'));
140
+ slot = trimmedBase.slice(slotIndex + 1).toLowerCase();
141
+ }
142
+ catch {
143
+ slot = '';
144
+ }
145
+ if (slot === 'desktop')
146
+ return 'desktop';
147
+ const hostProcess = globalThis.process;
148
+ if (hostProcess !== undefined && hostProcess !== null && Array.isArray(hostProcess.argv)) {
149
+ const desktopByArgv = hostProcess.argv.some((arg) => typeof arg === 'string' && arg.includes(DESKTOP_HOST_ARGV_MARKER));
150
+ if (desktopByArgv)
151
+ return 'desktop';
152
+ const execPath = typeof hostProcess.execPath === 'string' ? hostProcess.execPath : '';
153
+ const execBase = execPath.slice(Math.max(execPath.lastIndexOf('/'), execPath.lastIndexOf('\\')) + 1).toLowerCase();
154
+ const webByBinary = execBase === 'node' || execBase === 'node.exe' || execBase === 'nodejs' || execBase === 'nodejs.exe';
155
+ if (!webByBinary)
156
+ return 'desktop';
157
+ }
158
+ return 'web';
105
159
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@iasiv5/dsh-skip-browser-auth",
3
- "version": "0.3.6",
4
- "description": "替换官方 Connection 提供方跳过 BrowserAuth,免输 Token;已适配 0.1.2-rc.1、0.1.5-rc.1/rc.2、0.1.7-rc.1/rc.2 与 0.2.0-rc.1/rc.2,详见仓库。仅支持 web profile;desktop profile(存在 dsh-desktop-host 的环境)自动休眠,属设计行为。",
3
+ "version": "0.3.9",
4
+ "description": "替换官方 Connection 提供方跳过 BrowserAuth,免输 Token;已适配 0.1.2-rc.1、0.1.5-rc.1/rc.2、0.1.7-rc.1/rc.2 与 0.2.0-rc.1/rc.2,详见仓库。仅支持 web(宿主二进制须为 node);desktop 宿主一律自动休眠(多进程树判别:安装槽位/启动器 argv/宿主二进制),属设计行为。",
5
5
  "keywords": [
6
6
  "dsh",
7
7
  "deepseek-harness",