Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,44 @@
> 倒序排列,新版本条目在最上面。条目格式:`## vX.Y.Z — YYYY-MM-DD` + 类型(feat / fix / docs / chore)+ 要点 + 相关链接。
> 纪律见 AGENTS.md「变更记录纪律」:发版前先更新本文件并随版本提交;事故复盘、复现与真机验证记录也记在这里。

## Unreleased — 2026-10-13

**类型**:fix(route B 工具/预设生命周期、动态配置与客户端回归) / test。

- **复现与裁定**:两名 Sol 分区审查、Grok 交叉反证与主代理隔离复验确认了工具面、生命周期、
组合、代码扫描和客户端状态转换的问题;普通子 scope 的原始冲突仍由 v0.39.1 的 `shell` 隔离修复。
FastCtx 全文路径回显是既定策略,不误当文件工具替换问题;minimal 的 persistent PTY 不与全工具
变体的执行器来源混为一谈。只修既有功能,不新增功能、改 root shell 或市场 patch。
- **变体接线**:Windows 全工具 Git Bash 变体不再启用依赖 root PowerShell 的官方 `tool-bash`;
保留官方 pwsh 面,经同一行内补位通道接入私有 Git Bash 工具。minimal 的 persistent 通道保持。
- **工具生命周期**:存活 agent 的 `pwshTool` 双向翻转同步解除/重建自身的 restrict;异步导入/
挂载在插件或 agent 退役后不再迟到注册;jobs 服务卸载按真实 Cordis 活跃状态恢复前台 bash。
- **降级分支**:plain definition 的 required 标志递归转换为合法 JSON Schema;升级助手不可用
时仍解析并传递每会话 standing policy,不错误回退部署默认。两项为模块解析降级的受控复现,
不宣称本机生产已发生导入失败,也不改变 Windows 受限模式明示 unconfined 的既定取舍。
- **预设与默认值**:串行 reconcile 退役时关闭新任务、清理晚到 register 并等待 unregister;
动态关闭变体在退役前迁回部署默认,避免后续新会话引用不存在的 preset。既有钉住会话不迁移。
- **路径与终端**:run_code 扫描辨认关键字后的正则并在不确定时整份 no-op,不改正则数据;
FastCtx 入/出参获得 execution 上下文并服从总方言、委托与虚拟挂载开关;better-sidebar 的
adoptSidebar 动态翻转恢复接管前值并保护接管后的用户手选,不改变官方 autoTerminalShell
关后只停止未来写入的独立语义。
- **FastCtx 错误**:子进程/写管道 error 进入初始化 Promise,失败清理 pending 计时器与连接;
MCP `isError:true` 按官方桥约定抛错,不再把失败包装成宿主成功 value。
发现与连接启动采用 single-flight,拥有在途连接并在卸载时取消/等待,不让并发 agent 的发现流程
覆盖已在使用的 lazy client。该丢失所有权链已用受控 MCP 传输复现,并新增关闭/晚注册回归。
- **客户端**:设置卡所有 Hooks 在 readiness early-return 前调用;工具链进度轮询在网络/HTTP
暂态失败后重试,卸载或失效时取消,不因单次请求失败永久卡在忙碌状态。
- **回归入口**:新增可移植客户端/桥/运行时/宿主回归与实际 Core preview 集成用例,接入现有
`npm test`/`npm run test:preview`;不新增依赖、不跳过旧断言或 Node 22 矩阵。对齐上游 v0.39.1
(`521be01`)后完整测试 **156/156**、实际 Core 无头 preview **14/14**、打包 dry-run 和 diff 检查
通过。新增执行级回归保留 #21 的 real-array glob 契约与 stringified-array 响亮拒绝;不改已发布
版本号、历史记录或 npm 发布流程。Grok 对修复 diff 的只读反证未发现确定遗漏。
首轮全量检查发现三条旧源码正则仍锁旧实现,改为对应新保护条件并补同值/手选行为测试,没有删掉
保护断言。原 [Issue #19](https://github.com/KannaKuron/dsh-gitbash-shell/issues/19) 和
[PR #20](https://github.com/KannaKuron/dsh-gitbash-shell/pull/20) 已完成;这些后续修复由新 PR 承载,
Windows/Linux × Node 22/24 的远端结果见其检查。preview 不等同于完整 Electron GUI 或真 MCP
exe 并发验收,模块解析降级使用受控夹具复现。

## v0.39.1 — 2026-10-13

**类型**:fix(route B 私有执行器的真实服务隔离与读取授权) / test / ci。
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@
"icon.svg"
],
"scripts": {
"test": "node --test tests/smoke.mjs tests/private-executor.mjs",
"test:preview": "node --import ./tests/host-loader.mjs --test tests/preview.mjs",
"test": "node --test tests/smoke.mjs tests/private-executor.mjs tests/client-regressions.mjs tests/bridge-regressions.mjs tests/runtime-regressions.mjs tests/host-regressions.mjs",
"test:preview": "node --import ./tests/host-loader.mjs --test tests/preview.mjs tests/bridge-preview.mjs tests/runtime-preview.mjs tests/host-preview.mjs",
"version": "node -e \"const fs=require('fs');const m=JSON.parse(fs.readFileSync('dsh.plugin.json','utf8'));const v=JSON.parse(fs.readFileSync('package.json','utf8')).version;m.version=v;fs.writeFileSync('dsh.plugin.json',JSON.stringify(m,null,2)+'\\n')\" && git add dsh.plugin.json"
},
"keywords": [
Expand Down
41 changes: 26 additions & 15 deletions src/bash-tool.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,20 +248,29 @@ function outputSchemaSpec() {
}
}

/** Plain JSON-schema variant of the same union (fallback when defineTool is absent). */
function outputSchemaJson() {
const convert = (item) => {
const out = { type: item.type, additionalProperties: item.additionalProperties, properties: {} }
const required = []
for (const [key, value] of Object.entries(item.properties)) {
if (value.required === true) { required.push(key) }
const { required: _drop, ...rest } = value
if (rest.oneOf !== undefined) delete rest.required
out.properties[key] = rest
/** Convert key-level spec flags at every depth, including union branches. */
function schemaSpecToJson(spec) {
const { required: marker, ...out } = spec
if (Array.isArray(marker)) out.required = [...marker]
if (spec.properties !== undefined) {
const required = Array.isArray(marker) ? [...marker] : []
out.properties = {}
for (const [key, value] of Object.entries(spec.properties)) {
if (value.required === true && !required.includes(key)) required.push(key)
out.properties[key] = schemaSpecToJson(value)
}
return required.length > 0 ? { ...out, required } : out
if (required.length > 0) out.required = required
}
return { oneOf: outputSchemaSpec().oneOf.map(convert) }
for (const key of ['oneOf', 'anyOf', 'allOf']) {
if (Array.isArray(spec[key])) out[key] = spec[key].map(schemaSpecToJson)
}
if (spec.items !== undefined) out.items = schemaSpecToJson(spec.items)
return out
}

/** Plain JSON-schema variant of the same union (fallback when defineTool is absent). */
function outputSchemaJson() {
return schemaSpecToJson(outputSchemaSpec())
}

/** The official args validation (index.ts:73). */
Expand Down Expand Up @@ -320,8 +329,9 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
const defaultMode = executor.sandboxMode
const escalationReady = defaultMode !== undefined && typeof approveEscalation === 'function'
const escalationModes = escalationReady ? ESCALATION_TARGETS : []
// Missing approval helpers narrow escalation, not the session's standing policy.
const resolveSandboxPolicy = (exec) => {
if (!escalationReady) return undefined
if (defaultMode === undefined) return undefined
const policy = ctx.get('sandboxPolicy')
return policy ? policy.resolve(exec?.agent === undefined ? {} : { session: exec.agent.session }) : undefined
}
Expand Down Expand Up @@ -548,7 +558,7 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
// keys reach execute too, where enableRunInBackground enforces them
// ("Undeclared keys are allowed, so schema omission also needs
// enforcement", index.ts:498).
parameters: { type: 'object', properties: parametersSpec(), required: ['description', 'command'] },
parameters: schemaSpecToJson({ type: 'object', properties: parametersSpec() }),
output: {
schema: outputSchemaJson(),
render: (_args, value) => [{
Expand Down Expand Up @@ -580,7 +590,8 @@ export function createBashToolModule({ executor, toolsModule, sandboxModule, llm
jobCtx.effect(() => () => {
unregister()
try {
if (ctx.fiber?.state === 'active') foregroundOnly = ctx.tools.register(bashTool(undefined))
// Cordis FiberState.ACTIVE is the numeric enum member 2.
if (ctx.fiber?.state === 2) foregroundOnly = ctx.tools.register(bashTool(undefined))
} catch { /* the scope is going away */ }
})
})
Expand Down
61 changes: 36 additions & 25 deletions src/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -2063,6 +2063,42 @@ window.__ModuleLoader__.load({
if (scope && typeof scope.getSnapshot === "function") snap = scope.getSnapshot();
} catch (error_) { /* keep unavailable */ }

var pageView = props.view === "page";

// Hooks run for every snapshot state; readiness only gates their work.
useEffect(function () {
if (snap.status !== "ready" || (!open && !pageView)) return undefined;
if (toolsLoaded || toolBusy) return undefined;
loadTools();
return undefined;
}, [open, pageView, snap.status]);

/* Poll ONLY while a job is unfinished, and through the cheap ?job=1
form. A transient failure retries without discarding progress. */
useEffect(function () {
if (snap.status !== "ready" || toolJob === null || toolJob.finished) return undefined;
var alive = true;
var timer;
function schedule() {
timer = setTimeout(function () {
fetch(TOOLS_URL + "?job=1", { headers: { accept: "application/json" } })
.then(function (res) {
if (!res.ok) throw new Error("HTTP " + res.status);
return res.json();
})
.then(function (data) {
if (!alive) return;
var next = data !== null && typeof data === "object" && data.job ? data.job : null;
setToolJob(next);
if (next !== null && next.finished) loadTools();
})
.catch(function () { if (alive) schedule(); });
}, 1200);
}
schedule();
return function () { alive = false; clearTimeout(timer); };
}, [toolJob, snap.status]);

if (snap.status !== "ready") return null;

var value = snap.value || {};
Expand Down Expand Up @@ -2135,7 +2171,6 @@ window.__ModuleLoader__.load({
// dsh 0.1.6-alpha.2: the Plugins page renders this card through the
// `plugins.bundle.config` slot with view="page" — the page draws the
// title itself; the collapsible shell only shows on the popup seat.
var pageView = props.view === "page";

var mounts = snap.value.virtualMounts !== false;
var errs = snap.value.errorDialect !== false;
Expand Down Expand Up @@ -2469,30 +2504,6 @@ window.__ModuleLoader__.load({
.then(function () { setToolBusy(false); });
}

useEffect(function () {
if (!open && !pageView) return undefined;
if (toolsLoaded || toolBusy) return undefined;
loadTools();
return undefined;
}, [open]);

/* Poll ONLY while a job is unfinished, and through the cheap ?job=1
form: a full probe re-runs three winget commands per second. */
useEffect(function () {
if (toolJob === null || toolJob.finished) return undefined;
var timer = setTimeout(function () {
fetch(TOOLS_URL + "?job=1", { headers: { accept: "application/json" } })
.then(function (res) { return res.json(); })
.then(function (data) {
var next = data !== null && typeof data === "object" && data.job ? data.job : null;
setToolJob(next);
if (next !== null && next.finished) loadTools();
})
.catch(function () { /* keep the last known progress */ });
}, 1200);
return function () { clearTimeout(timer); };
}, [toolJob]);

var toolsSection = fold("sec.tools",
toolsData === null && toolErr === "" ? E("p", { className: "gb-hint" }, t("tools.loading")) : null,
toolErr !== "" ? E("p", { className: "gb-error" }, toolErr) : null,
Expand Down
18 changes: 11 additions & 7 deletions src/compositions.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
*
* Every variant mirrors the official 0.1.7 preset it replaces (packages/
* bundle/web-app/presets/{standard,minimal,ptc,cordis}.patch.yml) with the
* one Git Bash delta: the bash rows are always on and the pwsh rows are
* always off (the plugin ships its OWN bash tool over a private executor —
* on non-Windows hosts the official stack is native anyway, which is exactly
* what the old materialized variants encoded). A second, conditional delta exists while
* Git Bash routing: full variants keep the official platform-gated shell
* rows, and the inline patch adds OUR bash over a private executor on Windows.
* The minimal variant instead keeps its persistent Git Bash terminal rows.
* On non-Windows hosts the official stack stays native. A conditional delta exists while
* dsh-ptc-cordis-preset reports its experimental-CPython backend ACTIVE: the
* two workflow rows follow the official Python composition and go off (see
* `pythonActive` below). While that backend is off — the default, and also
Expand Down Expand Up @@ -56,7 +56,8 @@ const planSectionFor = (hostEra021) => hostEra021
* @param {'standard'|'ptc'|'cordis'} input.kind - which official preset this
* variant mirrors (workflow rows follow it: enabled on standard/cordis,
* disabled on ptc).
* @param {boolean} input.gitBash - bash rows always on, pwsh rows always off.
* @param {boolean} input.gitBash - retained for composition-call compatibility;
* full variants obtain Git Bash from the per-agent inline patch on Windows.
* @param {string|undefined} input.skillsDir - Creation authoring skills
* directory (cordis variant only; resolved beside the agent-preset package).
* @param {boolean} [input.pythonActive] - whether the experimental CPython
Expand All @@ -75,8 +76,11 @@ const planSectionFor = (hostEra021) => hostEra021
*/
export function pluginsFor({ kind, gitBash, skillsDir, pythonActive = false, hostExtras = {} }) {
const win = typeof process !== 'undefined' && process.platform === 'win32'
const bashDisabled = gitBash ? false : win
const pwshDisabled = gitBash ? true : !win
// Route B preserves the root PowerShell executor. On Windows the official
// tool-bash would execute through that executor, so full variants retain the
// official pwsh face and let the same per-agent inline patch add OUR bash.
const bashDisabled = win
const pwshDisabled = !win
const workflowOn = kind !== 'ptc' && pythonActive !== true
// dsh 0.2.1 added two rows to every full-tool official preset — time-context
// (durable clock readings) and tool-schedule (the reminder tools) — plus a
Expand Down
Loading
Loading