Compare commits

...
25 Commits
Author SHA1 Message Date
mesalogo f95acdc000 fix: restore packaged runtimes and ship macOS DMG only
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Deploy website to GitHub Pages / Deploy static website (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Desktop 0.12.7 omitted OpenCode's offline node_modules because the resource
copier excludes node_modules at its source root. Copy from the parent and
verify every prepared configuration file in the final package. Bundle DSH
session projection with the unpacked Host instead of leaving an ASAR-only
external import, and reject external DeepSeek imports in shipped chunks.

Run packaged OpenCode plugin import and the real DSH UtilityProcess handshake
in Windows x64 CI before removing unpacked output. Add regressions using the
actual electron-builder copier and Vite externalization configuration.

Prepare approved Desktop 0.12.8 notes and change macOS defaults and CI to DMG
only. Update aggregation, website downloads and update readers for 12
installers and 20 assets. Current readers retain historical DMG/ZIP support;
the user accepts that older macOS GitHub readers and all older mirror readers
need manual upgrades. Agent remains 0.11.21, with no new Agent or LoongArch tag.

Validation: 3608 tests passed, 0 failed, 61 skipped across 339 files;
typecheck, lint, release-note validation, website static checks and 18 website
tests passed. No local production build or packaged-app launch was performed
at the user's request; actual packaged startup validation runs in CI.

Release note: 修复安装后 OpenCode 离线依赖缺失和 DeepSeek Harness Host 启动退出;macOS 改为仅提供 DMG,受影响旧版用户需从下载页手动升级。
2026-09-08 14:17:38 +08:00
mesalogo 451b30d434 docs: design application and system tool navigation
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
2026-09-08 12:35:00 +08:00
mesalogo 25e897c22b chore: prepare GoodBuddy 0.12.7 and Agent 0.11.21 releases
Set the approved Desktop and Agent versions and package the approved Chinese
and English notes without changing published release history. Keep OpenCode
pinned to 1.18.29 and require Desktop 0.12.7 before the new Agent package.

Align both feature lists and affected design documents with reference-image
editing, inherited expert work modes, preview boundaries, and remaining
release acceptance. Application navigation remains design-only. No LoongArch
preview is planned for this release; Agent publication remains non-Latest.

The first candidate full run exposed a browser test clicking Send before it
was enabled. Wait for startup readiness using the existing test convention;
production code is unchanged by this release-preparation commit.

Validation: final full suite 3597 passed, 0 failed, 61 skipped across 338 files.
Typecheck, lint, bilingual release-note validation, version consistency,
unchanged historical notes, whitespace and relative-link checks passed.
No additional real model calls, local production builds, pushes or tags.
Concurrent navigation, workbar and roadmap design edits are not included.
2026-09-08 12:29:49 +08:00
mesalogo c8db340cc7 fix: prevent runtime permission stalls and Agent reconnect failures
Execute could stall on workspace-external access or native child-session
permissions, while structured questions could be lost or answered incorrectly.
Route owned OpenCode descendant interactions, correct Continue question and
custom-answer handling, and resolve DSH Execute ACP permissions without a
second approval. Ask remains read-only and unrelated sessions stay isolated.

Restore the required DSH session projection service. Reuse offline OpenCode
configuration by plugin version, remove unused npm shims from remote payloads,
normalize dependency modes, and accept real compound descriptors up to 4 MiB.
Fix intermittent Agent attach rejection and closed-handshake waits.

Add focused and real-Runtime regressions, document interaction boundaries and
validation evidence, and include the existing application/tool navigation
design drafts. Those navigation changes are design-only, not implemented UI.

Validation: full suite 3597 passed, 0 failed, 61 skipped across 338 files;
typecheck, lint, release-note verification, whitespace and relative-link checks
passed. Current-source Linux x64 installation, Ask/Execute, native subagent
external writes, reconnect and stop/bootstrap passed with 10 completed real
model calls, 13 native endpoint tests and 20 consecutive handshakes.
System sleep/wake and release CI packaging remain unverified.

Release note: 修复 Execute 访问工作区外目录或运行原生子代理时卡在权限等待的问题,并恢复 OpenCode 与 Continue 结构化问答的正确转交。修复远程 Agent 安装与重连失败;Ask 仍保持只读,任意终端密码或业务确认不会自动代答。
2026-09-08 11:34:45 +08:00
mesalogo 9aed926cda fix: stabilize OpenCode reuse and session switching
Check cached embedded servers before reuse, isolate concurrent cancellation, and serialize request MCP registration and cleanup. Preserve native controls across session switches and recover usable partial inventories.

Share the bundled offline OpenCode configuration with desktop and Agent launch paths, including model bridge environment forwarding.

Validation: focused tests and typecheck passed; lint passed before the final fixes. The full test run exceeded 120 seconds and reported a DS stdio MCP failure. Current-source Agent host and real suspend/resume validation remain pending.

Release note: 修复切换会话后 OpenCode 原生控件消失及并发取消相互影响的问题,并完善内嵌服务复用检查和桌面、Agent 的离线依赖配置。
2026-09-08 01:09:14 +08:00
mesalogo 86daae8be2 fix: remove terminal emulator padding 2026-09-07 18:08:52 +08:00
mesalogo 19086d656e fix: reuse bundled agent runtime resources 2026-09-07 16:57:59 +08:00
mesalogo d3947c79dd fix: avoid rerendering chat history while typing 2026-09-07 16:46:18 +08:00
mesalogo 6a1a671d3b fix: support reference image editing 2026-09-07 15:48:47 +08:00
mesalogo dbb7c2fc80 fix: align message content below avatars 2026-09-07 14:02:59 +08:00
mesalogo 237b8a8cee chore: prepare GoodBuddy 0.12.6 and Agent 0.11.20 releases
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Record approved bilingual notes for complete subagent progress, workspace diffs, knowledge selection, OpenCode stream cleanup, external command directories, and restored GitHub Agent updates. Remote progress and diff improvements require the new Agent package. No LoongArch preview is included.
2026-09-06 22:39:41 +08:00
mesalogo d08ba33c3e fix: complete subagent output and file workspace interactions
OpenCode child cards now show ordered text, reasoning and tool progress,
with the final result displayed separately. Local SDK and remote ACP paths
share child tracking, including progress retained across recovery and cancellation.
Remote live progress requires an Agent package containing the event plugin;
older Agent packages still expose final results without inventing missing history.

Knowledge-library rows remain clickable when native label activation changes
focus. File workspace changes use colored Git status letters and per-file
staged/unstaged diffs, including deleted, renamed and untracked files.
Refreshing reloads the selected diff and expanded directories, and more than
50 changes remain accessible through incremental loading.

Validated with 3552 passing tests, typecheck, lint, desktop build, current-source
Linux Host scenarios and isolated Windows desktop UI interactions.

Release note: 展开 OpenCode 子代理卡片可查看执行过程、工具详情与独立最终结果;知识库支持整行勾选。工作区支持单文件暂存与未暂存差异,刷新会更新当前差异,更多变更可继续加载。远端实时过程需要更新 GoodBuddy Agent。
2026-09-06 22:13:45 +08:00
mesalogo b03c25685c fix: clean up OpenCode streams and allow external working directories
Completed OpenCode requests left their event streams open across repeated and
parallel conversations. Each request now closes its own subscription on exit
without cancelling peer sessions, and cancellation preserves its original cause.

Direct-model Execute commands no longer restrict cwd to the workspace. Absolute
paths, parent-relative paths, and symlinks to external directories are accepted.
The workspace remains the default and relative-path base; Ask stays read-only,
and local execution binding, directory validation, and process cleanup remain.

Add real OpenCode lifecycle and real Shell tool-loop regressions, plus matching
feature documentation. Focused checks: 182 passed, 1 skipped; typecheck and lint
passed. Full shared-worktree suite: 3542 passed, 57 skipped, 1 unrelated sidebar
heading failure, reproduced separately and documented.

Release note: 修复 OpenCode 连续及并行对话结束后事件连接未释放的问题;直连模型 Execute 命令现在可直接指定工作区外的绝对路径、相对路径或符号链接目录,Ask 只读边界保持不变。
2026-09-06 19:53:42 +08:00
mesalogo 7003141e56 fix: restore GitHub Agent update checks with paginated discovery
The GitHub release history exceeded the catalog response limit, preventing users from checking Agent updates. Read ten releases per page and stop at the first eligible catalog while preserving the existing 100-release search window and signature checks.

Release note: 修复选择 GitHub 来源时 Agent 更新检查提示发布目录超出大小限制的问题;镜像更新和已有本地包保持不变。
2026-09-06 19:25:40 +08:00
mesalogo e692362414 chore: move ShareServer development to a standalone repository
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Remove the extracted ShareServer prototype and its server-specific documents
from Desktop. Link the shared network architecture to goodbuddy-shareserver,
while retaining Desktop ownership of the cross-product architecture.

Preserve local generated artifacts and ignore the old server build output.
Desktop runtime behavior and released user data are unchanged. Desktop tests,
typecheck, and lint passed; the standalone service was validated separately.
2026-09-06 19:02:41 +08:00
mesalogo ee804edcae fix: isolate desktop lint from ShareServer configuration
Cross-platform packages / Validate source (push) Canceled after 0s
GoodBuddy Agent Release / Authorize Agent release (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agent Release / Build darwin arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux arm64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Build linux x64 compound package (push) Canceled after 0s
GoodBuddy Agent Release / Sign cumulative Agent catalog (push) Canceled after 0s
GoodBuddy Agent Release / Publish Agent to GitHub and OSS (push) Canceled after 0s
Exclude the independent prototype from root lint and explicitly anchor TypeScript parser configuration to the Desktop repository. This fixes clean CI runners detecting both root and ShareServer configuration directories. Product behavior and approved release notes are unchanged.
2026-09-06 17:49:44 +08:00
mesalogo 85a4a374fa fix: keep ShareServer prototype tests outside desktop releases
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
Exclude the unfinished standalone ShareServer prototype from the Desktop root test suite and remove its CI dependency installation. Preserve its independent test configuration and document the release boundary. Approved Desktop and Agent release notes remain unchanged.
2026-09-06 17:37:37 +08:00
mesalogo 89a81dd07c fix: install ShareServer test dependencies in release CI
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
The root validation suite includes ShareServer tests, but clean CI runners only installed Desktop dependencies and could not resolve react-router-dom. Install the independently locked ShareServer dependencies before running validators. Release content and approved bilingual notes are unchanged.
2026-09-06 17:01:50 +08:00
mesalogo 0eb664c0b6 chore: prepare GoodBuddy 0.12.5 and Agent 0.11.19 releases
Cross-platform packages / Validate source (push) Canceled after 0s
Cross-platform packages / Authorize desktop packaging (push) Canceled after 0s
Cross-platform packages / linux arm64 (push) Canceled after 0s
Cross-platform packages / macos arm64 (push) Canceled after 0s
Cross-platform packages / windows arm64 (push) Canceled after 0s
Cross-platform packages / linux x64 (push) Canceled after 0s
Cross-platform packages / macos x64 (push) Canceled after 0s
Cross-platform packages / windows x64 (push) Canceled after 0s
Cross-platform packages / Publish GitHub and OSS release (push) Canceled after 0s
GoodBuddy Agents / Validate Agent source (push) Canceled after 0s
GoodBuddy Agents / Build darwin arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux arm64 Agent (push) Canceled after 0s
GoodBuddy Agents / Build linux x64 Agent (push) Canceled after 0s
Record approved bilingual release notes and align Desktop and Agent version metadata. Publish Desktop first so users can upgrade before the shared Agent catalog gains macOS arm64 entries. No LoongArch preview is included.
2026-09-06 16:44:46 +08:00
mesalogo 9c6d1611e2 feat: support macOS arm64 remote Agent hosts
Extend GoodBuddy Agent packaging, installation, SSH attachment, Runtime lifecycle, and Desktop package management to macOS arm64. Linux x64, Linux arm64, and Darwin arm64 share the existing signed catalog. Preserve Ask read-only behavior and Execute account permissions, and stop detached tool descendants during Mac cancellation.

Upgrade Desktop before publishing the first mixed-platform catalog because older Linux-only readers reject Darwin entries. Intel Macs are not supported; no launchd service or administrator access is required. This commit does not publish a release.

Validation: 3520 tests passed, 57 skipped; typecheck, lint, and development build passed. Real Mac installation, Ask/Execute, cancellation and reconnect passed; final Linux installation, Ask and reconnect passed. Nine real model requests were recorded.

Release note: 支持将 Apple Silicon Mac 用作远程项目 Host,可安装 macOS arm64 Agent 并使用 Ask、Execute 和断线重连;与 Linux 共用一个包目录,发布混合目录前需升级 Desktop。
2026-09-06 16:14:02 +08:00
mesalogo 1f01c26e04 fix: keep replies streaming while switching windows
Switching away from the desktop window and back could mark a reply that was
still being generated as "上次运行意外中断", freeze its output partway through,
and push the next message into the pending queue even though nothing had
actually failed. The conversation area also shifted on each switch because the
OpenCode runtime controls were unmounted and re-detected on every refresh.

Reading the conversation store no longer treats an in-progress reply as
interrupted; only startup recovery marks a run that an application exit truly
cut off. Runtime feature detection is now keyed on the resolved runtime scope,
so the OpenCode toolbar and manual Compact control stay in place across
refreshes. No request timeout or recovery behavior changed.

Release note: 修复来回切换桌面窗口时,正在生成的回复被误判为"上次运行意外中断"、输出中途停住、之后发送的消息只能进入待发送队列的问题。使用 OpenCode Runtime 时,会话区也不再因重复检测专属功能而抖动。
2026-09-05 16:28:45 +08:00
mesalogo dd8289fdef fix: make local knowledge bases ready for daily use
Knowledge selection was shared globally, document readiness overstated usable content, and important import and source-management paths exposed technical details or lacked safe recovery actions. Knowledge scope now persists per conversation, readiness reflects real processing channels, and the workspace guides users from creation and import into cited chat with controlled source actions.

Release note: 知识库现按对话保存使用范围,准确区分可用、处理中和失败文档,并提供更简洁的创建、检索测试、来源打开、删除确认和失败重试流程。
2026-09-05 15:51:07 +08:00
mesalogo d5fa0ec67c fix: keep long conversation content scrollable
Large messages containing reasoning, tool lists, or long replies could use an estimated off-screen height that left later content outside the reachable scroll range. Conversation messages now participate in layout with their actual height while the existing 80-message render window keeps long histories bounded.

Release note: 修复偶发的长会话滚动条提前到底问题;包含较长推理、工具记录或正文时,现在可以直接滚动查看全部已加载内容,无需切换会话刷新。
2026-09-05 14:51:21 +08:00
mesalogo 6eb73657e0 chore: ignore Python bytecode caches 2026-09-05 14:02:53 +08:00
mesalogo 4041edefa8 feat: add ShareServer console prototype
ShareServer previously existed only as design documents. The repository now includes a standalone Hono service and a responsive React management-console prototype covering administrator and member workflows, with local build, test, and container entry points.

The sharing design now includes organization-scoped digital agents and explicit local partitioned-memory selection for @ calls. Memory documentation consistently uses partitioned memory and treats time or internal events only as optional recall triggers.

This remains an interactive prototype: fixture actions do not persist, and production authentication, PostgreSQL isolation, device connections, routing, relay, and federation still require implementation.

Release note: 新增可独立运行的 ShareServer 管理控制台交互原型,并完善组织数字人与分区记忆共享设计;当前原型数据和操作不会写入生产系统。
2026-09-05 14:01:20 +08:00
237 changed files with 27960 additions and 22281 deletions
+35 -14
View File
@@ -45,17 +45,22 @@ jobs:
echo "version=$version" >> "$GITHUB_OUTPUT"
build:
name: Build Linux ${{ matrix.arch }} compound package
name: Build ${{ matrix.platform }} ${{ matrix.arch }} compound package
needs: authorize
if: needs.authorize.outputs.authorized == 'true'
strategy:
fail-fast: false
matrix:
include:
- arch: x64
- platform: linux
arch: x64
runner: ubuntu-24.04
- arch: arm64
- platform: linux
arch: arm64
runner: ubuntu-24.04-arm
- platform: darwin
arch: arm64
runner: macos-15
runs-on: ${{ matrix.runner }}
timeout-minutes: 25
environment:
@@ -76,11 +81,15 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Verify native build target
run: node -e "if(process.platform !== '${{ matrix.platform }}' || process.arch !== '${{ matrix.arch }}') throw new Error('Native Agent target mismatch')"
- name: Resolve locked native inputs
id: inputs
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
node - <<'NODE'
@@ -88,9 +97,11 @@ jobs:
const { createHash } = require('node:crypto')
const agent = require('./agent-runtime-lock.json')
const remote = require('./remote-runtime-lock.json')
const node = agent.node?.targets?.[`linux-${process.env.AGENT_ARCH}`]
const node = agent.node?.targets?.[`${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`]
const runtime = remote.runtimes?.opencode
const opencode = runtime?.targets?.[process.env.AGENT_ARCH]
const runtimeKey = process.env.AGENT_PLATFORM === 'linux'
? process.env.AGENT_ARCH : `${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`
const opencode = runtime?.targets?.[runtimeKey]
if (
!node ||
!opencode ||
@@ -136,7 +147,7 @@ jobs:
uses: actions/cache@v6
with:
path: ${{ runner.temp }}/goodbuddy-agent-release-inputs
key: goodbuddy-agent-release-${{ matrix.arch }}-${{ steps.inputs.outputs.cache-key }}
key: goodbuddy-agent-release-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.inputs.outputs.cache-key }}
- name: Acquire and verify native inputs
shell: bash
@@ -162,8 +173,14 @@ jobs:
"$NODE_URL" --output "$node_archive.partial"
mv "$node_archive.partial" "$node_archive"
fi
printf '%s %s\n' "$NODE_SHA256" "$node_archive" |
sha256sum --check --strict -
node - "$node_archive" "$NODE_SHA256" <<'NODE'
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const [archive, expected] = process.argv.slice(2)
if (createHash('sha256').update(readFileSync(archive)).digest('hex') !== expected) {
throw new Error('Node runtime archive integrity mismatch')
}
NODE
opencode_archive="$directory/$OPENCODE_ARCHIVE"
if [[ ! -f "$opencode_archive" ]]; then
@@ -201,6 +218,7 @@ jobs:
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
AGENT_VERSION: ${{ needs.authorize.outputs.version }}
GOODBUDDY_SIGNING_KEY_ID: ${{ vars.GOODBUDDY_SIGNING_KEY_ID }}
GOODBUDDY_SIGNING_PRIVATE_KEY: ${{ secrets.GOODBUDDY_SIGNING_PRIVATE_KEY }}
@@ -211,11 +229,12 @@ jobs:
output="$RUNNER_TEMP/goodbuddy-agent-output"
mkdir -p "$output"
node build/agent-package.cjs build \
--platform "$AGENT_PLATFORM" \
--arch "$AGENT_ARCH" \
--minimum-desktop-version "$(node -p "require('./package.json').version")" \
--node-archive "$RUNNER_TEMP/goodbuddy-agent-release-inputs/$NODE_ARCHIVE" \
--runtime-archive "$RUNNER_TEMP/goodbuddy-agent-release-inputs/$OPENCODE_ARCHIVE" \
--output "$output/goodbuddy-agent-$AGENT_VERSION-linux-$AGENT_ARCH.gbagent" \
--output "$output/goodbuddy-agent-$AGENT_VERSION-$AGENT_PLATFORM-$AGENT_ARCH.gbagent" \
> "$output/result.json"
node - "$output/result.json" "$AGENT_ARCH" "$AGENT_VERSION" <<'NODE'
const { readFileSync } = require('node:fs')
@@ -223,9 +242,10 @@ jobs:
const result = JSON.parse(readFileSync(resultPath, 'utf8'))
if (
result.descriptor?.architecture !== architecture ||
result.descriptor?.platform !== process.env.AGENT_PLATFORM ||
result.descriptor?.version !== version ||
result.archive !==
`goodbuddy-agent-${version}-linux-${architecture}.gbagent` ||
`goodbuddy-agent-${version}-${process.env.AGENT_PLATFORM}-${architecture}.gbagent` ||
!/^[a-f0-9]{64}$/u.test(result.sha256)
) {
throw new Error('Production Agent package result is invalid')
@@ -235,7 +255,7 @@ jobs:
- name: Upload verified compound package
uses: actions/upload-artifact@v7
with:
name: goodbuddy-agent-package-linux-${{ matrix.arch }}
name: goodbuddy-agent-package-${{ matrix.platform }}-${{ matrix.arch }}
path: ${{ runner.temp }}/goodbuddy-agent-output/goodbuddy-agent-*.gbagent
if-no-files-found: error
compression-level: 0
@@ -269,7 +289,7 @@ jobs:
- name: Download compound packages
uses: actions/download-artifact@v8
with:
pattern: goodbuddy-agent-package-linux-*
pattern: goodbuddy-agent-package-*
path: dist/agent-release
merge-multiple: true
@@ -353,6 +373,7 @@ jobs:
node build/agent-catalog.cjs create \
--x64-package "dist/agent-release/goodbuddy-agent-$AGENT_VERSION-linux-x64.gbagent" \
--arm64-package "dist/agent-release/goodbuddy-agent-$AGENT_VERSION-linux-arm64.gbagent" \
--darwin-arm64-package "dist/agent-release/goodbuddy-agent-$AGENT_VERSION-darwin-arm64.gbagent" \
"${previous[@]}" \
--catalog dist/agent-release/agent-catalog.json \
--signature dist/agent-release/agent-catalog.sig \
@@ -489,8 +510,8 @@ jobs:
run: |
set -euo pipefail
root="https://goodbuddy.oss-cn-beijing.aliyuncs.com/agent-releases/v$AGENT_VERSION"
for architecture in x64 arm64; do
name="goodbuddy-agent-$AGENT_VERSION-linux-$architecture.gbagent"
for target in linux-x64 linux-arm64 darwin-arm64; do
name="goodbuddy-agent-$AGENT_VERSION-$target.gbagent"
curl --fail --silent --show-error --location \
"$root/$name" --output "$RUNNER_TEMP/$name"
cmp "dist/agent-release/$name" "$RUNNER_TEMP/$name"
+54 -14
View File
@@ -9,6 +9,8 @@ on:
- 'build/agent-bundle.cjs'
- 'build/agent-package.cjs'
- 'build/agent-ci-bundle.cjs'
- 'build/agent-build-target.cjs'
- 'build/binary-architecture.cjs'
- 'build/file-hash.cjs'
- 'build/remote-runtime-bundle.cjs'
- 'build/signing-key-preflight.cjs'
@@ -19,6 +21,7 @@ on:
- 'src/agent-daemon/**'
- 'src/shared/**'
- 'tests/agent-bundle.test.ts'
- 'tests/agent-darwin-build.test.ts'
- 'tests/agent-package.test.ts'
- 'tests/agents-workflow.test.ts'
- 'tests/remote-runtime-bundle.test.ts'
@@ -31,6 +34,8 @@ on:
- 'build/agent-bundle.cjs'
- 'build/agent-package.cjs'
- 'build/agent-ci-bundle.cjs'
- 'build/agent-build-target.cjs'
- 'build/binary-architecture.cjs'
- 'build/file-hash.cjs'
- 'build/remote-runtime-bundle.cjs'
- 'build/signing-key-preflight.cjs'
@@ -41,6 +46,7 @@ on:
- 'src/agent-daemon/**'
- 'src/shared/**'
- 'tests/agent-bundle.test.ts'
- 'tests/agent-darwin-build.test.ts'
- 'tests/agent-package.test.ts'
- 'tests/agents-workflow.test.ts'
- 'tests/remote-runtime-bundle.test.ts'
@@ -73,22 +79,28 @@ jobs:
run: |
npm test -- \
tests/agent-bundle.test.ts \
tests/agent-darwin-build.test.ts \
tests/agent-package.test.ts \
tests/remote-runtime-bundle.test.ts \
tests/agents-workflow.test.ts
npm run typecheck
build:
name: Build Linux ${{ matrix.arch }} Agent
name: Build ${{ matrix.platform }} ${{ matrix.arch }} Agent
needs: validate
strategy:
fail-fast: false
matrix:
include:
- arch: x64
- platform: linux
arch: x64
runner: ubuntu-24.04
- arch: arm64
- platform: linux
arch: arm64
runner: ubuntu-24.04-arm
- platform: darwin
arch: arm64
runner: macos-15
runs-on: ${{ matrix.runner }}
timeout-minutes: 20
@@ -103,17 +115,23 @@ jobs:
- name: Install dependencies
run: npm ci
- name: Verify native runner target
run: node -e "if (process.platform !== '${{ matrix.platform }}' || process.arch !== '${{ matrix.arch }}') throw new Error('Agent CI requires the native target')"
- name: Resolve locked official Node runtime
id: runtime
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
node - <<'NODE'
const { appendFileSync } = require('node:fs')
const lock = require('./agent-runtime-lock.json')
const target = lock.node?.targets?.[`linux-${process.env.AGENT_ARCH}`]
const { lock } = require('./build/agent-ci-bundle.cjs').readCiLocks(
process.env.AGENT_PLATFORM, process.env.AGENT_ARCH
)
const target = lock.node?.targets?.[`${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`]
if (
!target ||
!/^[A-Za-z0-9._-]+\.tar\.gz$/u.test(target.archive) ||
@@ -147,7 +165,7 @@ jobs:
uses: actions/cache@v6
with:
path: ${{ runner.temp }}/goodbuddy-agent-node
key: goodbuddy-agent-ci-node-${{ matrix.arch }}-${{ steps.runtime.outputs.sha256 }}
key: goodbuddy-agent-ci-node-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.runtime.outputs.sha256 }}
- name: Acquire and verify locked Node runtime
shell: bash
@@ -170,22 +188,34 @@ jobs:
"$NODE_URL" --output "$archive.partial"
mv "$archive.partial" "$archive"
fi
printf '%s %s\n' "$NODE_SHA256" "$archive" |
sha256sum --check --strict -
node - "$archive" "$NODE_SHA256" <<'NODE'
const { createHash } = require('node:crypto')
const { readFileSync } = require('node:fs')
const [archive, expected] = process.argv.slice(2)
if (createHash('sha256').update(readFileSync(archive)).digest('hex') !== expected) {
throw new Error('Node runtime archive integrity mismatch')
}
NODE
- name: Resolve locked OpenCode Runtime
id: opencode
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
run: |
set -euo pipefail
node - <<'NODE'
const { appendFileSync } = require('node:fs')
const { createHash: hash } = require('node:crypto')
const lock = require('./remote-runtime-lock.json')
const runtime = lock.runtimes?.opencode
const target = runtime?.targets?.[process.env.AGENT_ARCH]
const { runtimeLock } = require('./build/agent-ci-bundle.cjs').readCiLocks(
process.env.AGENT_PLATFORM, process.env.AGENT_ARCH
)
const runtime = runtimeLock.runtimes?.opencode
const key = process.env.AGENT_PLATFORM === 'linux'
? process.env.AGENT_ARCH
: `${process.env.AGENT_PLATFORM}-${process.env.AGENT_ARCH}`
const target = runtime?.targets?.[key]
if (
!runtime ||
!target ||
@@ -213,7 +243,7 @@ jobs:
uses: actions/cache@v6
with:
path: ${{ runner.temp }}/goodbuddy-opencode-runtime
key: goodbuddy-agent-ci-opencode-${{ matrix.arch }}-${{ steps.opencode.outputs.cache-key }}
key: goodbuddy-agent-ci-opencode-${{ matrix.platform }}-${{ matrix.arch }}-${{ steps.opencode.outputs.cache-key }}
- name: Acquire and verify locked OpenCode Runtime
shell: bash
@@ -251,16 +281,22 @@ jobs:
NODE
- name: Run native Agent regressions
if: matrix.platform == 'linux'
run: |
npm test -- \
src/agent-daemon/model-bridge.test.ts \
src/agent-daemon/lifecycle.test.ts
- name: Run native macOS process regressions
if: matrix.platform == 'darwin'
run: npm test -- src/agent-daemon/darwin-process-identity.test.ts
- name: Build and verify ephemeral signed compound Agent package twice
id: agent
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
NODE_ARCHIVE: ${{ steps.runtime.outputs.archive }}
OPENCODE_ARCHIVE: ${{ steps.opencode.outputs.archive }}
run: |
@@ -270,11 +306,12 @@ jobs:
mkdir -p "$directory"
agent_version="$(node -p "require('./agent-runtime-lock.json').agentVersion")"
node build/agent-ci-bundle.cjs \
--platform "$AGENT_PLATFORM" \
--arch "$AGENT_ARCH" \
--node-archive "$RUNNER_TEMP/goodbuddy-agent-node/$NODE_ARCHIVE" \
--opencode-archive "$RUNNER_TEMP/goodbuddy-opencode-runtime/$OPENCODE_ARCHIVE" \
--output-directory "$directory/bundle" \
--archive "$directory/goodbuddy-agent-$agent_version-linux-$AGENT_ARCH.gbagent" |
--archive "$directory/goodbuddy-agent-$agent_version-$AGENT_PLATFORM-$AGENT_ARCH.gbagent" |
tee "$directory/result.json"
node - <<'NODE'
const { appendFileSync, readFileSync } = require('node:fs')
@@ -286,6 +323,7 @@ jobs:
)
if (
result.architecture !== process.env.AGENT_ARCH ||
result.platform !== process.env.AGENT_PLATFORM ||
!/^[a-f0-9]{64}$/u.test(result.archiveSha256)
) {
throw new Error('Agent CI result is invalid')
@@ -300,14 +338,16 @@ jobs:
shell: bash
env:
AGENT_ARCH: ${{ matrix.arch }}
AGENT_PLATFORM: ${{ matrix.platform }}
AGENT_SHA256: ${{ steps.agent.outputs.sha256 }}
run: |
{
echo "### Linux $AGENT_ARCH Agent"
echo "### $AGENT_PLATFORM $AGENT_ARCH Agent build"
echo
echo "Native compound Agent, Node, and OpenCode Runtime build, test-signature verification, launch smoke, and deterministic archive comparison passed."
echo
echo "Archive SHA-256: \`$AGENT_SHA256\`"
echo
echo "The ephemeral CI key and bundle are not published."
echo "Native build checks do not replace real Host installation, Attach, and Ask/Execute acceptance."
} >> "$GITHUB_STEP_SUMMARY"
+2 -2
View File
@@ -248,7 +248,7 @@ jobs:
- name: Build, sign and notarize macOS release packages
if: matrix.platform == 'macos' && steps.macos-signing.outputs.enabled == 'true'
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --skip-build
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --format dmg --skip-build
env:
ELECTRON_CACHE: ${{ runner.temp }}/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/electron-builder
@@ -261,7 +261,7 @@ jobs:
- name: Build unsigned macOS release packages
if: matrix.platform == 'macos' && steps.macos-signing.outputs.enabled == 'false'
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --skip-build --unsigned
run: npm run release:package -- --platform ${{ matrix.platform }} --arch ${{ matrix.arch }} --format dmg --skip-build --unsigned
env:
ELECTRON_CACHE: ${{ runner.temp }}/electron
ELECTRON_BUILDER_CACHE: ${{ runner.temp }}/electron-builder
+4
View File
@@ -1,6 +1,8 @@
node_modules/
out/
dist/
# Local build output left by the extracted ShareServer project.
shareserver/build-server/
coverage/
.vite/
.runtime-resources/
@@ -8,6 +10,8 @@ coverage/
.agent-resources/
*.log
*.tsbuildinfo
__pycache__/
*.py[cod]
.env
.env.*
!.env.example
+37 -10
View File
@@ -56,6 +56,14 @@ OpenCode/Continue 用例默认读取 `dist/harness-package-probe/win-unpacked`
调用。测试不会输出 API Key,文件操作在临时工作区中执行。文件包含经 Main 回环
broker 调用已分配自定义 MCP 的真实 OpenCode 和 Continue 用例。
不使用外部模型的 Runtime 原生交互回归位于
`src/main/agent/opencode-runtime-permissions.test.ts`、
`src/main/agent/continue-runtime-permissions.test.ts` 和
`src/main/agent/deepseek-harness-acp-e2e.test.ts`。它们用本地确定性模型驱动真实 Runtime
或固定 Host,覆盖工作区外访问、原生问答及 Ask 边界。OpenCode 用例默认使用开发缓存
二进制;可通过 `GOODBUDDY_TEST_OPENCODE_BINARY` 指定已经安装的候选二进制,避免把
仍在运行的旧缓存版本当作候选版本验证。这不替代当前源码 Agent 的 Linux Host 验收。
## 生产构建
生成 Electron Main、Preload 和 Renderer 生产文件:
@@ -145,7 +153,7 @@ npm run dist:linux:arm64
组件,缺少远端包也不能阻塞普通桌面打包或发布。
“设置 > 平台功能 > 远程项目(技术预览)”列出用户数据目录中已经下载或导入的
Linux x64/arm64 复合 Agent 包。首次打开和手动刷新会从“关于与更新”所选来源读取
Linux x64、Linux arm64 和 macOS arm64 复合 Agent 包。首次打开和手动刷新会从“关于与更新”所选来源读取
小型签名目录,显示本地版本、在线最新兼容版本以及 `有更新 / 已是最新 / 未下载 /
校验失败`;此检查不会下载 `.gbagent`。只有用户点击“下载”或“更新到”后才传输包,
用户也可导入或导出 `.gbagent` 离线包。
@@ -211,6 +219,9 @@ package format v1 归档已经包含固定 Agent 和 Node,可直接用于 Host
桌面包会携带经过版本与完整性校验的 OpenCode、Continue 和 DSH 插件安装 Runtime:
- OpenCode 平台二进制来自 `.runtime-resources/<arch>`。
- Desktop 的离线配置复制以 `.runtime-resources` 为源、`opencode-config{,/**/*}` 为过滤器,避免 electron-builder 对复制源根目录下 `node_modules` 的固定排除。最终包逐文件核对离线配置与依赖;DSH bootstrap 和 chunk 不得残留外部 `@deepseek-ai/*` 导入。Windows x64 CI 在删除解包目录前实际导入包内 OpenCode 插件并运行隔离的 DSH UtilityProcess 握手与 npm 探针,失败即停止发布。
- OpenCode 离线配置位于 `.runtime-resources/opencode-config`,构建时按已安装 `@opencode-ai/plugin/package.json` 的版本复用,不依赖根 lockfile 或自定义 ready/integrity 标记。缺失、不可读或版本不匹配时,在 staging 中执行精确版本 npm 安装,成功后替换缓存;安装失败保留旧缓存并清理 staging。本机共享配置同样比较随包插件与缓存插件的版本,版本不同时通过 staging 替换。
- 离线依赖安装禁用 npm bin links;远程 Runtime 组包排除不使用的 `.bin` 命令链接,并把复制后的普通依赖文件统一为 `0644`,OpenCode 入口仍为 `0755`。目录元数据、每个依赖文件及许可证继续纳入签名校验。
- Continue Runtime 来自锁定版本的 `@continuedev/cli`。
- DSH 插件安装使用精确锁定并从 `app.asar` 解包的 npm CLI,通过当前 Electron 的 Node 模式运行;最终用户不需要另装 Node.js 或 npm。
- DSH 图片输入使用精确锁定的 `@napi-rs/canvas` 完整解码 JPEG/PNG。通用包与目标平台、目标架构的 Skia 原生包必须从 `app.asar` 解包;当打包 Runner 的架构与目标架构不同时,发布脚本会根据 lockfile 的精确版本、下载地址和 integrity 临时暂存目标原生包,完成后清理。发布校验会检查版本、目标架构和 MIT 许可证。
@@ -254,16 +265,32 @@ node build/agent-package.cjs build \
Agent 源码、共享协议、lock、bundle 工具和测试与桌面应用保持在同一仓库、同一 commit;
不维护长期分叉的 Agent 源码分支。`.github/workflows/agents.yml` 在相关 Pull Request、
`main` push 和手工触发时,分别使用 `ubuntu-24.04` 与 `ubuntu-24.04-arm` 原生构建
Linux x64/arm64 复合包。每个 job 从 `agent-runtime-lock.json` 解析官方 Node HTTPS
Linux x64/arm64 复合包,并使用 `macos-15` 原生构建 Darwin arm64
复合包。所有 job 从 `agent-runtime-lock.json` 解析官方 Node HTTPS
地址与 digest,并从 `remote-runtime-lock.json` 解析固定 OpenCode npm 包与 integrity;
下载后再次校验,随后使用仅存在于进程内的临时 Ed25519 测试 key 构建两次,完成内外层
测试签名验证、Agent 原生启动 smoke 和确定性 `.gbagent` 对比。
测试签名验证、Agent CLI 加载 smoke 和确定性 `.gbagent` 对比。所有目标还验证
Koffi 原生模块加载与 OpenCode `--version`;Linux job 继续运行 Agent 原生回归。
Darwin arm64 的 Node 和 OpenCode 固定输入分别由同一份
`agent-runtime-lock.json` 与 `remote-runtime-lock.json` 提供,不再维护 CI 专用副本。
组包器区分 ELF 与 Mach-O,Darwin CI 产物命名为
`goodbuddy-agent-<version>-darwin-arm64.gbagent`,不构建 Intel Mac 目标。
macOS Host 继续通过 SSH 按需启动 detached Agent,不注册 launchd 或开机服务。
进程身份使用 macOS 原生接口,Unix Socket 使用 `getpeereid`;Host 下载兼容系统自带
`shasum -a 256`,不要求安装 GNU coreutils。真实 Host 验证记录见
[远程主机技术设计](./docs/features/remote-host/technical-design.md)。
三个目标共用 `agent-catalog.json` 和现有签名格式,条目按平台与架构区分。旧版 Desktop
只接受 Linux catalog 条目;首次发布包含 Darwin 的累计 catalog 前必须先发布新版
Desktop,并在发布说明中要求用户升级 Desktop。旧版的在线 Agent 目录检查会失败,
本地已验证包不被删除。不得为此改写历史包或复用发布标签。
该验证 workflow 不读取 production signing secret,不修改公开 key registry,也不上传
可安装 Agent 工件。`.github/workflows/agent-release.yml` 是唯一 production Agent
发布路径:只接受指向受保护 `main` 历史的 annotated
`agent-v${agent-runtime-lock.agentVersion}` 标签,在 `agent-signing` Environment 中
原生构建两种架构,生成并签名累计目录,然后同步到非 Latest 的 GitHub Agent Release
原生构建 Linux x64、Linux arm64、macOS arm64 三个目标,生成并签名累计目录,然后同步到非 Latest 的 GitHub Agent Release
与北京 OSS。该 Environment 只保护一组 GoodBuddy 通用发布变量与 Secret:
- `GOODBUDDY_SIGNING_KEY_ID` /
@@ -284,7 +311,7 @@ OSS 先写 `agent-releases/v<version>/` 不可变对象,GitHub Release 公开
### 远程 OpenCode Runtime 基础
`remote-runtime-lock.json` 独立锁定首个远程 Runtime:OpenCode 1.18.9 的 Linux x64
`remote-runtime-lock.json` 独立锁定首个远程 Runtime:OpenCode 1.18.29 的 Linux x64
baseline 与 arm64 官方包 integrity、`bin/opencode` 入口和固定 `acp` 参数。它不同于
`agent-runtime-lock.json`,后者锁定 Agent 自带 Node 以及用于 Linux `SO_PEERCRED`
的 Koffi 版本。Agent bundle 将 Koffi loader 作为 external module,并只携带目标
@@ -371,8 +398,8 @@ DeepSeek Harness Runtime、`app.asar`、目标架构和安装包签名。
npm run release:package -- --platform macos --arch <x64|arm64> --unsigned
```
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG 与
ZIP,以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
默认发布产物为 Windows 的 NSIS 安装包与 portable ZIP、macOS 的 DMG,
以及 Linux 的 AppImage、DEB 与 RPM。Linux 原生 Runner 必须安装
`rpm`/`rpmbuild` 工具后再调用 electron-builder。Windows portable ZIP 解压后可直接
运行 `GoodBuddy.exe`,并包含启用便携数据目录的
`.goodbuddy-portable.json`。每个目标目录都包含带文件大小和 SHA-256 的
@@ -408,8 +435,8 @@ Desktop 正式发布必须把候选分支推送与标签推送拆成两个阶段
全部目标成功后,才会严格校验并聚合所有平台产物,生成按平台重命名的
manifests、总 `release-manifest.json` 和 `SHA256SUMS`。随后工作流通过
GitHub OIDC 获取短期 STS 凭据,将发布资产和 `site-release.json` 上传到
北京 OSS 的不可变版本目录,并公开校验 14 个安装包。验证通过后才创建或
更新 draft GitHub Release、上传 22 个 Release 资产并正式发布,最后原子
北京 OSS 的不可变版本目录,并公开校验 12 个安装包。验证通过后才创建或
更新 draft GitHub Release、上传 20 个 Release 资产并正式发布,最后原子
切换官网 `latest.json`。任一步失败都不会提前切换官网最新版本。
同一标签重跑时,工作流会根据 `resources/release-notes.json` 重新生成并
@@ -456,7 +483,7 @@ git push github "$tag"
macOS 发布 job 会先原子判断 Apple 凭据状态:以下五项 Actions Secrets 全部
存在时,使用 Developer ID Application 证书签名,并通过 App Store Connect
API Key 提交 Apple notarization;五项全部缺失时,明确生成未签名、未公证的
DMG 和 ZIP,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时
DMG,并在 Actions 日志与摘要中警告 Gatekeeper 限制;只配置一部分时
任务失败,不能静默降级为未签名包。
- `MACOS_CERTIFICATE_BASE64`:包含证书及私钥的 `.p12` 文件经 Base64 编码后的内容。
+83 -25
View File
@@ -17,6 +17,12 @@ otherwise.
- [x] **Cross-platform desktop application**: Supports Windows, macOS, and
Linux release targets on `x64` and `arm64`.
- [x] **Chinese hardware and operating-system coverage**: Standard Linux
packages target compatible x64 and arm64 environments, including UOS, Kylin,
Hygon, Zhaoxin, Kunpeng, and Phytium systems. This is not vendor certification.
LoongArch has a separate experimental loong64 preview outside standard
releases and automatic updates; no preview is planned for 0.12.8. See the
[preview boundaries](./docs/development/loongarch-preview-build.md).
- [x] **Configurable global shortcut**: Enable, disable, or record an Electron
accelerator under Platform Features / General. The default remains
`CommandOrControl+Shift+Space`; if registration conflicts or saving fails,
@@ -32,6 +38,10 @@ otherwise.
Host heading and only the remote path on each project row.
- [x] **File, screenshot, window, and clipboard context**: Added to model
context only after explicit user selection.
- [x] **Per-file workspace diffs**: Changed files expose separate staged and
unstaged diffs, including deleted, renamed, and untracked files. Refresh
reloads the selected diff and expanded directories; more than 50 changes
remain accessible through incremental loading.
- [x] **Rich responses**: Supports GitHub Flavored Markdown, LaTeX math,
constrained Mermaid diagrams, and static in-conversation HTML previews.
Complete HTML and HTML code blocks can be previewed in place after an Agent
@@ -55,26 +65,35 @@ otherwise.
- [ ] **Project Agent Space** (planned): Unifies roles, knowledge, Skills/MCP,
models, approval policy, budgets, and timeouts in a Project, with reusable
templates.
- [ ] **Application navigation and system-tool shortcuts** (design only):
Plans fixed system entries, configurable built-in application visibility,
and footer shortcuts into the existing workbar. These are not implemented
interface changes. See the [design](./docs/features/application-tool-navigation/README.md).
- [ ] **Additional assistant workbar and execution-space capabilities**
(planned): Builds on the current workbar and multiple terminals with
supervision, unified Runtime monitoring, managed processes, safe static HTML
preview, target-pinnable workspace/browser/artifact instances, bottom
docking, and separate windows. Task Center remains the singleton Task index;
attachments and knowledge remain in the conversation composer, while future
memory and historical execution context belong to the associated Task. See
attachments and knowledge remain in the conversation composer, while memory,
when implemented, and historical execution context belong to the associated Task. See
the [Feature PRD](./docs/features/assistant-workbar/prd.md).
### Agent Runtimes and model connections
- [x] **Direct model Runtime**: Supports question answering, knowledge
synthesis, controlled tool execution, and image generation.
synthesis, controlled tool execution, image generation, and reference-image
editing through providers supporting the OpenAI-compatible image-editing
endpoint. Editing sends attached images as multipart data and still requires
validated inline image output rather than fetching provider-returned URLs.
- [x] **Direct model programming agent**: Local direct text models can run the
platform Shell in Execute mode and delegate one level of programming
Subagent work while inheriting the parent request's mode, model, workspace,
and capability scope. OpenCode, Continue, DeepSeek Harness, and managed SSH
do not receive duplicate copies of these tools. The Windows local command
path and a real-model edit, test, fix, and review loop have passed; native
macOS and Linux command validation remains.
macOS and Linux command validation remains. Execute commands can select
absolute, relative, or symbolic-link directories outside the workspace;
the workspace stays the default and relative-path base. Ask remains read-only.
- [x] **OpenCode and Continue**: Use isolated child processes, an environment
variable allowlist, unified configuration, cancellation, total execution
limits, bounded streaming output, and activity records. Shared process
@@ -86,7 +105,10 @@ otherwise.
credential without starting a throwaway server; the explicit settings test,
native inventory still performs full startup and health checks, then shares
that execution Runtime with the first real request instead of cold-starting
another server. Execution Runtimes remain reusable per project. GoodBuddy-managed local OpenCode keeps
another server. Execution Runtimes remain reusable per project, while local
OpenCode configuration dependencies and content-addressed Skill snapshots are
shared globally across projects. Session data, tool output, and request-scoped
MCP state remain isolated. GoodBuddy-managed local OpenCode keeps
requests in the same conversation ordered, while different conversations in
the same project and different projects can run in parallel. Request-scoped
dynamic MCP tools remain isolated through default wildcard disablement and
@@ -110,8 +132,8 @@ otherwise.
directories, and create Ask or Execute projects. Both modes start the
signed Runtime directly without bubblewrap. Ask applies OpenCode's Ask
permission configuration and permits only native read approvals at the
Agent tool-dispatch boundary, while Execute receives no such override and
retains all permissions of the selected SSH account. The Agent owns accepted Prompts,
Agent tool-dispatch boundary, while Execute explicitly allows native permissions
and retains all permissions of the selected SSH account. The Agent owns accepted Prompts,
provider/tool rounds, Runtime processes, a stable model ledger, and a bounded
semantic transcript over a private Unix socket and ACP v5. Work continues on
the Host after Desktop exit, network loss, or local-process termination.
@@ -123,12 +145,16 @@ otherwise.
in the desktop package; switching projects updates only local selection and
does not connect or download. Settings reads a small signed catalog and
reports the local version, latest compatible online version, and update
state for Linux x64/arm64. Downloads occur only after a user action, and
state for Linux x64, Linux arm64, and macOS arm64. Intel Macs are not supported.
Downloads occur only after a user action, and
packages can also be imported or exported offline. Each compound `.gbagent`
package contains the Agent, fixed Node, and the compatible OpenCode Runtime
maintained by the desktop source. Online sources follow the GitHub/Beijing
OSS choice in About and Updates. The cumulative signed catalog binds minimum
Desktop version, Agent protocol, architecture, size, SHA-256, and fixed URL.
Desktop version, Agent protocol, platform, architecture, size, SHA-256, and fixed URL.
All three targets share one catalog. Users must upgrade Desktop before the
first mixed-platform catalog is published because older Linux-only readers
reject catalogs containing Darwin entries.
The public-key registry accepts equivalent JSON whitespace and line endings
while still strictly validating schema, Ed25519 keys, environment, and
revocation; catalog, package, manifest, payload signatures, and streaming
@@ -148,9 +174,11 @@ otherwise.
failure, Agent `SIGKILL`/restart, and recovery from a reopened Desktop SQLite
database. Successful tool START/END events appear exactly once, with no
Prompt, provider, or tool replay observed. The current Agent source lock is
`0.11.18`, while the current Desktop release candidate is `0.12.4`; formal
`0.11.21`, while the current Desktop release candidate is `0.12.8`; formal
publication status follows the separate Agent and Desktop
release channels.
release channels. Current macOS source has passed native package installation,
detached lifecycle, Attach, real Ask/Execute, and cancellation of tools in
separate process groups on a real Host; this does not imply publication.
- [x] **Manual SSH Host environment provisioning source path**: After Host Key,
authentication, and system probes succeed, GoodBuddy saves the Host and
read-only probes the shared Agent/Runtime. Saving a Host or opening a project
@@ -177,10 +205,12 @@ otherwise.
capabilities, and prompt startup do not scan the full payload. See the
[design](./docs/features/remote-host/environment-provisioning-technical-design.md).
- [ ] **Real-Host acceptance for SSH Host environment provisioning**: The
source can use the current format-v1 package. GitHub, Beijing mirror, Linux
x64/arm64, cancellation, and offline GoodBuddy transfer validation remains.
This status does not mean the path is published or has completed real-Host
testing.
current source passed isolated Linux x64 package installation, Ask/Execute,
native subagent external writes, reconnection, and stop/bootstrap.
The complete Host-card acquisition matrix across GitHub, Beijing mirror,
Linux x64/arm64, cancellation, and offline GoodBuddy transfer remains.
Candidate CI/native packaging and system sleep/wake are not yet verified;
this partial development evidence does not imply publication.
- [x] **DeepSeek Harness (preview)**: Uses the fixed GoodBuddy Host and an
OpenAI-compatible model connection. It prefers an administrator-provided
connection, otherwise follows the compatible default model or first
@@ -202,11 +232,25 @@ otherwise.
code is not subject to Ask initialization isolation.
- [x] **Ask and Execute work modes**: Ask remains read-only. Execute is the
user's authorization for all tools, processes, network access, and writable
paths available to the current local or SSH account.
paths available to the current local or SSH account, including paths outside
the workspace and native subagent work.
- [x] **Native Runtime interaction routing**: OpenCode and Continue questions
support choices, yes/no, free-text answers, and skipping through the existing
question card. OpenCode also routes questions from owned child sessions.
Execute permission confirmations are handled automatically rather than
waiting for another approval. See the [interaction boundaries](./docs/features/assistant-workbar/runtime-interactions.md).
- [x] **Experts and Subagents**: Supports explicit experts, team analysis, and
up to three read-only experts running in parallel. Chat shows each
up to three experts running in parallel. Experts inherit the parent
Ask/Execute mode and can use enabled local direct-model tools; Ask remains
read-only. They are not remote OpenCode child sessions. Chat shows each
expandable full expert response first and the main Agent's synthesis below,
and persists both with the conversation.
- [x] **OpenCode child progress and final results**: Child cards show ordered
text, reasoning, and tool progress separately from the final result. Remote
live progress requires Agent `0.11.20`; older packages still expose final
results without reconstructing missing progress.
- [x] **OpenCode event-stream cleanup**: Each completed or cancelled request
closes its own subscription without cancelling parallel conversations.
- [x] **Role-bound model connections**: Each role can inherit the default model
or select an independent text-model connection. Invalid connections safely
fall back to the default; the synthesis role always inherits the default.
@@ -300,10 +344,10 @@ otherwise.
supervision across OpenCode, Continue, and DeepSeek Harness. Users select
only Conversation or Task; Job/Run remains internal rather than becoming a
tree or independent object.
- [ ] **Executable Subagents** (planned): Explicit Execute delegation with
bounded nesting, parallelism, tokens, time, and tool permissions, aggregated
by Task in the workbar's fixed Runtime section with cancellation and audit
ownership.
- [ ] **Advanced Subagent supervision** (planned): Task-level aggregation in
the workbar's fixed Runtime section, configurable nesting, parallelism,
budgets, and lifecycle controls. Basic expert-mode inheritance and
single-level direct-model programming delegation are already available.
### Skills, MCP, and knowledge
@@ -353,7 +397,11 @@ otherwise.
report an experimental Resource catalog; the current Continue version
explicitly does not support Resources.
- [x] **Local knowledge bases**: Supports file, directory, and web imports,
SQLite FTS5 retrieval, and source tracing.
SQLite FTS5 retrieval, and source tracing. Knowledge selection persists per
conversation and starts empty for new conversations. Advanced creation and
retrieval parameters are collapsed by default. Documents distinguish ready,
processing, and failed states, with source opening, retry, and confirmation
before source removal.
- [ ] **External knowledge-base connections** (planned): Manage Dify, FastGPT,
and RAGFlow instances from the existing Knowledge page, then discover and
bind remote knowledge bases with provider-specific retrieval settings and
@@ -432,8 +480,8 @@ otherwise.
created and edited for Global or one or more selected Projects. Legacy
single-project settings migrate without loss, and project-level memory and
action output must explicitly target a Project in scope. Task Center and
Settings no longer duplicate the form. Future partitioned memory remains an
independently designed long-term direction. See the
Settings no longer duplicate the form. Partition-aware review, candidate
generation, and recall triggers remain to be designed. See the
[Smart Heartbeat PRD](./docs/features/smart-heartbeat/prd.md).
- [ ] **General supervision** (planned): Uses a fixed supervision section to
observe user-selected conversations, Tasks, automations, or experiments and
@@ -519,7 +567,17 @@ otherwise.
build commands.
- [x] **Six-target native release matrix**: Native runners build Windows,
macOS, and Linux `x64` and `arm64` targets with release manifests and SHA-256
hashes. Linux also produces AppImage, DEB, and RPM.
hashes. Windows provides NSIS and portable ZIP, macOS DMG only, and Linux
AppImage, DEB, and RPM, for 12 installers and 20 published assets.
- [x] **Desktop Runtime package verification**: Offline OpenCode dependencies
are copied without electron-builder's root node_modules exclusion; DSH
session projection is bundled into the unpacked Host. Final package checks
compare the offline dependency tree and reject external DeepSeek imports.
Windows x64 CI additionally imports the packaged OpenCode plugin and checks
the real DSH UtilityProcess handshake before publishing.
- [x] **DMG-only update manifests**: Current Desktop and website readers accept
DMG-only and historical DMG/ZIP manifests. Older macOS GitHub readers and all
older mirror readers require a manual upgrade from the download page.
### Open interfaces, team collaboration, and remote execution
+20 -11
View File
@@ -14,28 +14,34 @@
### 桌面基础、工作空间与上下文
- [x] **跨平台桌面应用**:支持 Windows、macOS、Linux,以及 `x64`、`arm64` 发布目标。
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
- [x] **信创软硬件覆盖**:正式 Linux 安装包覆盖国产 `x64` 与 `arm64` 架构,可用于统信 UOS、银河麒麟及海光、兆芯、鲲鹏、飞腾等兼容环境。龙芯 LoongArch 另行提供 `loong64` 实验预览版,不属于正式发布矩阵,也未纳入自动更新;`0.12.8` 不构建该预览。这里的覆盖范围不等同于厂商或整机兼容认证,具体状态与限制见[龙芯预览版构建说明](./docs/development/loongarch-preview-build.md)。
- [x] **可配置全局快捷唤起**:在“平台功能 / 通用设置”中启停或录制 Electron accelerator;默认保留 `CommandOrControl+Shift+Space`,冲突或保存失败时继续使用上一组已注册快捷键,并显示可处理的状态。
- [x] **Projects、独立对话与会话分支**:按项目隔离上下文,管理会话、附件和 Git 工作区变更;本地会话可在稳定状态下复制当前聊天内容到独立分支,分支持续显示来源徽标且不复制 Task、队列或成果归属;项目选择器区分本地、托管 SSH 与远程消息通道项目,托管 SSH 项目按 Host 分组并在 Host 标题显示真实 Agent 连接状态,项目行只保留远端路径。
- [x] **文件、截图、窗口、剪贴板上下文**:用户明确选择后才加入模型上下文。
- [x] **工作区单文件差异**:变更文件可分别查看已暂存和未暂存差异,覆盖删除、重命名和未跟踪文件;刷新会重读当前差异和已展开目录,超过 50 项变更可继续加载。
- [x] **富文本回答**:支持 GitHub Flavored Markdown、LaTeX 数学公式、受控 Mermaid 图表和会话内 HTML 静态预览。Agent 完成回复后,完整 HTML 与 HTML 代码块可在原位置预览,并通过图标按钮查看源码或打开全屏预览;该能力默认开启,可在“平台功能 / 通用设置”关闭,预览不执行脚本、联网、表单提交或窗口操作。
- [x] **AI 回复与完整会话复制**:已完成的 AI 回复可从消息底部复制不含推理、工具日志和引用元数据的 Markdown 原文;完整会话复制复用同一条经过 Preload/Main 校验的剪贴板路径。
- [x] **助手工作栏、多终端与可调布局**:右侧工作栏使用持久“+”能力目录和应用 Tab;用户可为当前本机或托管 SSH 项目打开多个独立终端,获得有界输出、调整尺寸、结束与显式重连。关闭终端 Tab 会结束 Shell,应用重启只恢复已结束的 Tab 描述,不自动重启 Shell。主侧栏和魔法笔记列表支持鼠标与键盘调宽,魔法笔记列表可隐藏并记住布局,窄宽度下自动堆叠。
- [ ] **项目 Agent Space**(规划中):在 Project 中统一角色、知识、Skills/MCP、模型、审批策略、预算和超时,并支持模板复用。
- [ ] **应用导航与系统工具快捷入口**(仅设计):规划固定系统入口、内置应用显隐和打开现有工作栏的底部工具按钮;尚未实现新界面。详见[设计文档](./docs/features/application-tool-navigation/README.md)。
- [ ] **通用助手工作栏与执行空间后续能力**(规划中):在现有工作栏和多终端基础上继续加入监督、统一 Runtime、受管进程、静态安全 HTML 预览、可固定目标的工作区/浏览器/成果实例、底部停靠与独立窗口。Task Center 保持 Task 的单例索引,附件与知识库继续由会话输入区管理,未来的记忆与历史执行上下文归入关联 Task。详见 [Feature PRD](./docs/features/assistant-workbar/prd.md)。
### Agent Runtime 与模型连接
- [x] **直连模型 Runtime**:支持问答、知识总结、受控工具执行和图像生成。
- [x] **直连模型 Runtime**:支持问答、知识总结、受控工具执行、图像生成,以及通过支持 OpenAI 兼容图片编辑接口的服务商进行参考图编辑。编辑使用 multipart 上传附件图片,结果仍只接受经过校验的内联图片,不下载服务商返回的图片 URL。
- [x] **直连模型编程 Agent**:本机直连文本模型可在 Execute 模式运行平台 Shell,并可按父请求模式、模型、工作区和能力范围委派一层编程 Subagent;OpenCode、Continue、DeepSeek Harness 和托管 SSH 不重复注入这两个工具。Windows 本机命令与真实模型“修改、测试、修复、复核”闭环已通过,macOS 与 Linux 真机命令仍待对应平台验收。
- [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、总执行时限、有界流式输出和活动记录;共享进程回收逻辑保留 Windows 完整进程树终止,并对采用独立进程组的 POSIX 子进程执行组回收。聊天状态检查使用一次性探测 Runtime 并在返回后立即回收,不进入按模型与项目隔离的执行 Runtime 缓存;本机 GoodBuddy 管理的 OpenCode 在该被动检查中只确认所选路径和模型凭据,不启动用后即毁的 Server,设置中的显式连接测试和原生能力清单仍执行完整启动及健康检查,原生能力清单随后与首次实际请求复用同一个执行 Runtime,不再重复冷启动;执行 Runtime 仍按项目复用。本机 GoodBuddy 管理的 OpenCode 只对同一会话内的请求保持顺序,不同会话即使属于同一项目也可并行,不同项目同样可并行;请求级动态 MCP 通过默认通配禁用和当前请求精确放行保持隔离。OpenCode 与 Continue 不再按固定工具调用数或活动数中止单次运行,已观察到的工具与 Subagent 活动全部保留在本地会话,不再在原生任务继续执行时丢弃后续详情。交互提问只由前台对话回答,定时任务、远程通道和委派等后台执行遇到提问时会立即失败并提示改为前台运行,避免无限等待。本机 OpenCode 启动时直接使用已选择的绝对路径,不预先检查文件或运行 `--version`,路径失效时由实际启动返回错误;托管 Linux ARM Host 激活 Runtime 时复用准备阶段已验证的 registry 和 manifest,不重新哈希或检查完整 OpenCode 二进制。
- [x] **托管 SSH OpenCode 闭环(技术预览)**:该能力由“设置 > 平台功能”中的独立“远程项目(技术预览)”页签控制并默认关闭;关闭时不影响任何本地项目、普通桌面功能或桌面发布。启用后可管理固定 Host Key 的 SSH Host、浏览有界远端目录并创建 Ask/Execute 项目;两种模式都直接启动签名 Runtime,不依赖 bubblewrap。Ask 同时使用 OpenCode Ask 权限配置,并在 Agent 工具分发边界只允许原生读取审批;Execute 不注入该权限覆盖,使用所选 SSH 账号的完整权限。Agent 通过私有 Unix socket 和 ACP v5 持有 accepted Prompt、Provider/工具轮次、Runtime 进程、稳定模型 ledger 与有界语义 transcript;Desktop 退出、网络中断或本机进程结束后 Host 继续执行,重启后只 attach 原 controller/binding/operation 并把 provenance 原子归并回原对话,不重发 Prompt。模型 profile 与 API Key 只在当前 accepted operation 生命周期内进入 Agent 内存,不写入 Renderer、SSH 参数、远端环境或磁盘。远端组件不嵌入桌面包,项目切换只更新本地选择且不会连接或下载;设置页只读取小型签名目录并按 Linux x64/arm64 显示本地版本、在线最新兼容版本和“有更新/已是最新”,用户点击后才下载签名 `.gbagent`,也可导入/导出离线包。每个复合包包含 Agent、固定 Node 与由桌面源码统一维护适配的 OpenCode Runtime;在线来源跟随“关于与更新”的 GitHub/北京 OSS 选择,签名累计目录绑定最低桌面版本、Agent 协议、架构、大小、SHA-256 和固定 URL。公钥 registry 接受等价 JSON 空白和换行,但仍严格校验 schema、Ed25519 key、环境与撤销;目录、包、manifest、payload 签名和流式 SHA-256 不变。缺少某架构包只使该架构托管 SSH 不可用。项目只保存 Host、远端路径、Runtime 选择和模式;首次实际使用 Workspace/Runtime 时读取 Host current identity,同一进程内其他项目复用已确认 identity 和 Agent 连接。托管 SSH 会话只显示支持的 OpenCode 选择。同一 Host 可并行运行多个项目和对话;Agent-owned Prompt 不因固定模型调用次数或 Prompt 累计输出 Token 数停止,GoodBuddy 输出缓冲区达到容量时使用传输背压而不取消 Runtime;恢复进度分别显示网络、Agent、Runtime、已提交事件 cursor、完成或失败/重试,只阻塞受影响项目。Linux x64 真实 Host 已覆盖短/长 detach、本机 harness 强制结束、并发、SSH relay 丢失、取消、确定/不确定 Provider 失败、Agent `SIGKILL`/重启和 reopened Desktop SQLite 恢复;成功工具 START/END 各一次,未发现 Prompt、Provider 或工具重放。当前 Agent 源码 lock 为 `0.11.18`,Desktop 发布候选为 `0.12.4`;正式发布状态以 Agent 与 Desktop 的独立发布渠道为准。
- [x] **OpenCode 与 Continue**:使用隔离子进程、环境变量白名单、统一配置、取消、总执行时限、有界流式输出和活动记录;共享进程回收逻辑保留 Windows 完整进程树终止,并对采用独立进程组的 POSIX 子进程执行组回收。聊天状态检查使用一次性探测 Runtime 并在返回后立即回收,不进入按模型与项目隔离的执行 Runtime 缓存;本机 GoodBuddy 管理的 OpenCode 在该被动检查中只确认所选路径和模型凭据,不启动用后即毁的 Server,设置中的显式连接测试和原生能力清单仍执行完整启动及健康检查,原生能力清单随后与首次实际请求复用同一个执行 Runtime,不再重复冷启动;执行 Runtime 仍按项目复用,但 OpenCode 配置依赖和按内容摘要生成的 Skill 快照在所有本机项目间全局复用,会话数据、工具输出及请求级 MCP 状态继续隔离。本机 GoodBuddy 管理的 OpenCode 只对同一会话内的请求保持顺序,不同会话即使属于同一项目也可并行,不同项目同样可并行;请求级动态 MCP 通过默认通配禁用和当前请求精确放行保持隔离。OpenCode 与 Continue 不再按固定工具调用数或活动数中止单次运行,已观察到的工具与 Subagent 活动全部保留在本地会话,不再在原生任务继续执行时丢弃后续详情。交互提问只由前台对话回答,定时任务、远程通道和委派等后台执行遇到提问时会立即失败并提示改为前台运行,避免无限等待。本机 OpenCode 启动时直接使用已选择的绝对路径,不预先检查文件或运行 `--version`,路径失效时由实际启动返回错误;托管 Linux ARM Host 激活 Runtime 时复用准备阶段已验证的 registry 和 manifest,不重新哈希或检查完整 OpenCode 二进制。
- [x] **托管 SSH OpenCode 闭环(技术预览)**:该能力由“设置 > 平台功能”中的独立“远程项目(技术预览)”页签控制并默认关闭;关闭时不影响本地项目、普通桌面功能或桌面发布。支持 Linux x64、Linux arm64 和 macOS arm64 Host,不支持 Intel Mac;通过 SSH 按需启动 detached Agent,不要求开机服务。Ask 在 Runtime 工具边界保持只读,Execute 使用所选 SSH 账号的完整权限。Agent 持有 Prompt、模型和工具轮次、Runtime 进程及语义 transcript,Desktop 断开后重新 Attach 原操作而不重发 Prompt;模型凭据仅在当前操作内存中使用,不写入 Renderer、SSH 参数、环境或磁盘。设置页按平台和架构显示包状态,用户显式下载、更新或导入/导出包含 Agent、固定 Node 和 OpenCode 的签名 `.gbagent`;三个目标共用一个签名 catalog,继续校验签名和 SHA-256。首次发布混合 catalog 前需先升级 Desktop,旧版 Linux-only 目录读取器无法解析包含 Darwin 的目录。macOS 原生安装、生命周期、Attach、真实 Ask/Execute 和独立进程组工具取消已在真实 Host 验证;这不等于新版本已经发布。详细运行边界和 Linux 历史回归证据见[远程主机技术设计](docs/features/remote-host/technical-design.md),正式发布状态以 Agent 与 Desktop 的独立发布渠道为准。
- [x] **SSH Host 手动环境准备源码链路**:Host Key、认证和系统探针成功后先保存 Host,并只读探测共享 Agent/Runtime;保存 Host、打开项目都不自动安装。Host 卡片只有一个按版本事实显示“安装远程环境”“更新远程环境”或“重新安装”的主按钮,次级 SegmentedControl 选择默认且不持久化的“自动”、Host 下载或 GoodBuddy 传输;“版本匹配”badge 不等同环境健康。自动模式只在 operation/prepare 前探测并择一,显式选择保持有效,任何 prepare、commit 或 adoption 失败都不跨 acquisition 自动 fallback。两种方式把同一签名 compound `.gbagent` 交付到固定 staging 后,共用 control-plane prepare、commit、Agent activate/health、Runtime activate、finalize 与显式 cleanup。GoodBuddy 路径可在同一次操作下载并验证缺失候选、缓存并取得 lease,再有界流式 SFTP 上传一个归档和其中已验证的 bootstrap Node,不把约 294 MiB 整包读入 Main `Buffer`;Host 在解包时完成一次完整 payload 校验。未完成操作只记录暂存 cleanup 所需的 operation ID;下次更新尽力清理旧暂存后重新 prepare,不保存远端 metadata 副本,也不让 cleanup 失败阻塞新更新或回滚健康环境。已有项目在实际使用 Workspace/Runtime 时按需解析 Host current identity 并执行固定 `attach-or-bootstrap`,注册后的 health、capabilities 和 prompt 启动不扫描完整 payload。详见 [设计说明](./docs/features/remote-host/environment-provisioning-technical-design.md)。
- [ ] **SSH Host 环境准备真实 Host 验收**:源码已可使用当前 format v1 包;仍需完成 GitHub、北京镜像、Linux x64/arm64、取消和离线 GoodBuddy 传输的真实 Host 验收。当前状态不表示已发布或已完成真实 Host 测试。
- [ ] **SSH Host 环境准备真实 Host 验收**:当前源码已在隔离 Linux x64 环境通过包安装、Ask/Execute、原生子代理工作区外写入、重连及 stop/bootstrap。Host 卡片的完整 GitHub、北京镜像、Linux x64/arm64、取消和离线 GoodBuddy 传输矩阵仍待验收,候选 CI/原生打包与系统休眠唤醒也尚未验证;部分开发证据不代表已经发布。
- [x] **DeepSeek Harness(预览)**:使用 GoodBuddy 固定 Host 和 OpenAI 兼容模型连接;优先使用管理员提供的连接,否则跟随兼容的默认模型或首个兼容连接,无需单独重复选择。Ask 只允许调用 Host 中真实注册的 `read`、`skill` 以及 Main 管理的 Web Search/Fetch 代理,拒绝插件同名冒充,Execute 放行全部已启用内置及插件工具,并以当前用户权限运行。图像输入跟随所选模型连接的能力声明,文本模型在 Host 或模型调用前拒绝图片,图片模型通过有界内联内容和临时 Attachment Store 接收 JPEG/PNG。
- [x] **DSH npm 插件市场**:市场默认关闭,由用户显式开启后搜索公共 npm 的 `dsh-plugin` 包,使用捆绑 npm 执行精确版本安装和普通 lifecycle scripts,并支持启停、JSON 配置、移除、失败启动自动停用和离线管理已安装插件;关闭市场只隐藏目录与管理界面,不改变已有插件的启停状态,第三方代码不受 Ask 初始化隔离。
- [x] **Ask 与 Execute 工作模式**:Ask 保持只读;Execute 是用户对当前本机或 SSH 账号可用工具、进程、网络和可写路径的完整授权。
- [x] **专家与 Subagent**:支持显式专家、团队分析和最多三个只读专家并行分析;聊天先展示可逐项展开的专家完整输出,再在其下展示总 Agent 的综合结果,并随会话保存。
- [x] **Ask 与 Execute 工作模式**:Ask 保持只读;Execute 是用户对当前本机或 SSH 账号可用工具、进程、网络和可写路径的完整授权,包括工作区外路径及原生子代理工作。
- [x] **Runtime 原生交互转交**:OpenCode 与 Continue 的选择、yes/no、自由文本回答和跳过使用现有问答卡片,OpenCode 同时转交属于当前请求的子会话提问。Execute 权限确认自动处理,不等待第二次审批;具体支持范围见[交互边界](./docs/features/assistant-workbar/runtime-interactions.md)。
- [x] **专家与 Subagent**:支持显式专家、团队分析和最多三个专家并行分析。专家继承父请求 Ask/Execute 模式,可使用已启用的本机直连模型工具,Ask 仍只读;这些工具不是远程 OpenCode 子会话。聊天先展示可逐项展开的专家完整输出,再在其下展示总 Agent 的综合结果,并随会话保存。
- [x] **OpenCode 子代理过程与最终结果**:子代理卡片按顺序显示文字、推理和工具过程,最终结果独立展示。远程实时过程需要 Agent `0.11.20`,旧包仍显示最终结果但不补造缺失过程。
- [x] **OpenCode 事件连接回收**:请求结束或取消后关闭自身订阅,不取消其他并行会话。
- [x] **直连模型命令工作目录**:Execute 命令可通过绝对路径、相对路径或符号链接使用工作区外目录;工作区仍为默认目录和相对路径基准,Ask 保持只读。
- [x] **角色绑定模型连接**:每个角色可继承默认模型或选择独立文本模型连接,失效连接安全回退默认模型,综合角色始终继承默认模型。
- [x] **多协议模型配置**:支持 Anthropic Messages、OpenAI Responses、OpenAI Chat Completions、OpenAI Images 和无认证本机模型;新用户默认连接为本机 Ollama 兼容地址,不预置第三方云模型服务。升级时只替换从未配置凭据且仍完全等于旧内置值的历史默认连接,用户显式保存或加密凭据的连接保持不变;已有部署仅使用兼容环境变量提供凭据时继续沿用对应的历史连接参数,通用模型环境变量仍优先。“保存并测试模型”会发送有界的真实文本或图片生成请求并校验生成结果,而不是只检查 HTTP 连通性,因此可能产生少量服务商用量费用。
- [x] **模型请求定制**:每个 LLM 连接可填写有界 JSON 自定义请求头和顶层请求体。直连与 Continue 支持两者;本地 OpenCode 和 DeepSeek Harness 只使用各自原生支持的请求头,受管 SSH OpenCode 模型网关支持两者。Runtime、协议和认证字段始终优先,这些普通连接设置不能用于保存 API Key 或其他密钥。详见[模型连接请求定制](./docs/features/model-connections/README.md)。
@@ -46,7 +52,7 @@
- [x] **Continue Runtime 定制**:提供静态配置中的原生 Rules、Prompt 模板与 MCP 清单,以及可编辑的 GoodBuddy Rules/Prompt 配置预设;聊天可按请求选择预设和填入可继续编辑的 Prompt。当前 Continue Host 没有可信的静态原生 Tool 发现接口,且使用隔离的 `CONTINUE_GLOBAL_DIR`,因此界面明确标记 Tools 不支持静态发现,也不把 Host 实际不会加载的工作区或用户 Skills 冒充原生能力;GoodBuddy 分配的 Skills 仍按请求暂存执行。Continue 临时 Host 不复用原生会话压缩,手动压缩由 GoodBuddy 摘要模型完成并验证持久化摘要覆盖范围;Agent 交互提问转换为统一问答卡片。Resources、Hooks、后台 Job 和 Continue 原生会话管理继续暂缓。
- [x] **Runtime 原生清单语义**:原生能力以 Agents、Tools、Commands、Skills、MCP、Rules、Prompts、Resources、LSP、Formatters 和上下文 11 个页签展示;清单状态独立于 Runtime 连通性,区分完整、部分、不可用、仅连接和不支持。DeepSeek Harness 通过 Host Registry 枚举有界的内置/插件 Tools 与 Skills,显示真实 Ask/Execute 边界,并排除 GoodBuddy 按请求分配的 Skills、Web/MCP 代理。
- [ ] **Runtime 监督栏目**(规划中):在应用级助手工作栏的固定 Runtime 栏目统一承载 OpenCode、Continue 和 DeepSeek Harness 的 Task 级委派、后台执行、Workflow/Hook、长任务与原生会话监督;用户只选择 Conversation 或 Task,Job/Run 保持内部,不形成树或独立操作对象。
- [ ] **可执行 Subagent**(规划中):提供显式 Execute 委派,限制嵌套、并行、Token、时间和工具权限,在助手工作栏固定的 Runtime 栏目按 Task 聚合状态、取消入口和审计归属。
- [ ] **Subagent 高级监督**(规划中):在工作栏固定 Runtime 栏目按 Task 聚合,提供可配置的嵌套、并行、预算和生命周期控制。基础专家模式继承与单层直连模型编程委派已经可用。
### Skills、MCP 与知识库
@@ -56,7 +62,7 @@
- [x] **内置 MCP 按需接入**:知识库、魔法笔记、GoodBuddy 配置与内置浏览器 MCP 可分别启停,并可分配给直连模型、GoodBuddy 管理的 OpenCode 和 Continue;DeepSeek Harness 在设置中明确显示为暂不支持。内置 MCP 仅通过当前请求的短期本机权限提供,Ask / Execute 读写边界不受用户配置放宽。
- [x] **MCP Tools**:显式启用的自定义 MCP 可按 Runtime 分配给直连模型、GoodBuddy 管理的 OpenCode、Continue Agent Execute 和 DeepSeek Harness,并仅在 Execute 加载;Agent 子进程只获得按请求签发的本机回环权限,MCP 地址、命令和凭据保留在 Main,动态工具仍会重新发现并经过现有执行记录与权限边界。
- [x] **MCP Prompts 与 Resources 元数据**:MCP 测试仅在 Server 声明对应能力时发现有界的 Prompt、参数与 Resource 元数据,不读取 Resource 内容;Runtime 支持的 Prompt 可填入聊天草稿后继续编辑。OpenCode 可报告实验性 Resource 清单,Continue 当前版本明确不支持 Resources。
- [x] **本地知识库**:支持文件、目录和网页导入、SQLite FTS5 检索及来源追溯。
- [x] **本地知识库**:支持文件、目录和网页导入、SQLite FTS5 检索及来源追溯。知识范围按对话保存,新对话默认不选择知识库;创建与检索高级参数默认折叠。文档区分可用、处理中和失败,支持打开来源、失败重试及移除来源前确认。
- [ ] **外部知识库接入**(规划中):在现有知识库页面管理 Dify、FastGPT 和 RAGFlow 实例;新建知识库时可读取并绑定远端知识库,保留 Provider 专属检索配置和引用。外部系统只负责知识检索,不接入其 App、Chat、Workflow 或 Agent,也不批量同步、本地索引或修改远端内容。被回答引用的有界检索片段随会话保存在本机。详见[外部知识库接入 PRD](./docs/features/knowledge-base/external-knowledge-prd.md)。
- [x] **知识图谱**:支持规则、模型和混合抽取,以及实体、关系、别名和证据维护。
- [x] **向量模型配置与检索**:可配置兼容 Embeddings 接口并用于语义检索。
@@ -74,7 +80,7 @@
- [x] **任务、活动与成果**:集中管理任务状态、审计活动和独立成果文件;普通聊天回复只保留在会话中,不再自动复制到成果栏,已有重复聊天 Markdown 从成果列表隐藏但不物理删除。运行记录改由 Main SQLite 保存,不再受旧 Renderer 的 500 条、单详情 4,000 字符或约 2 MB 上限影响,页面按批继续显示;迁移旧缓存时若历史可能已被截断,会持续明确提示,不能把既有缺口伪装成完整。Token 用量按 Runtime 与模型归类,并针对 OpenAI 兼容与 Anthropic Messages 的不同上报口径归一化展示缓存命中率;活动按会话分组并默认收起,避免长历史占满页面。
- [x] **Task 与定制任务体验**:每个产品级 Task 只关联一条 Conversation,一条 Conversation 可承载多个 Task;左侧会话列表通过行首展开按钮显示带共享状态点的 Task 子项,父会话行不重复任务标签,UI 只展示到 Task,不暴露 Job/Run 层级。新建定制任务可关联当前或新 Conversation,默认 Execute 并沿用 Runtime、工具和审批边界;重复触发复用同一 Task,文本结果回写 Conversation,独立文件和图片保留为成果。普通消息与到期 Scheduled Task 共用 Conversation 级持久发送队列,同一会话一次只执行一项;当前回复期间仍可继续发送,队列按顺序续跑,并允许删除或“立即中断并插入”。Task Center 继续作为完整索引,不建设独立 Automation Center。当前计划触发支持单次、每日和每周;高级时区、Cron、事件触发与重试治理仍按 PRD 逐步实现。详见 [Task Center PRD](./docs/features/task-and-job/task-center-prd.md) 和 [Scheduled Task PRD](./docs/features/task-and-job/scheduled-task-prd.md)。
- [x] **记忆与智能心跳**:当前提供周期回顾、建议记忆、洞察、后续任务和可审计运行轨迹。
- [x] **智能心跳入口与范围改善**:将“智能心跳 > 心跳计划”作为完整配置的唯一权威入口,支持创建和编辑 Global 或指定一个、多个 Project 的计划;旧单项目配置无损迁移,项目级记忆与行动输出必须显式指定范围内的 Project。Task Center 和设置不再复制心跳表单。“未来分区记忆”仍只是尚待独立设计的长期方向。详见 [智能心跳 PRD](./docs/features/smart-heartbeat/prd.md)。
- [x] **智能心跳入口与范围改善**:将“智能心跳 > 心跳计划”作为完整配置的唯一权威入口,支持创建和编辑 Global 或指定一个、多个 Project 的计划;旧单项目配置无损迁移,项目级记忆与行动输出必须显式指定范围内的 Project。Task Center 和设置不再复制心跳表单;按分区回顾、候选生成和唤起条件仍待设计。详见 [智能心跳 PRD](./docs/features/smart-heartbeat/prd.md)。
- [ ] **通用监督**(规划中):通过固定监督栏目观察用户选择的会话、任务、自动化或实验对象,提供带证据的评论与人工介入请求,但不自动发言、批准工具或切换 Execute。详见 [会话监督 PRD](./docs/features/conversation-supervision/prd.md)。
- [ ] **批量运行与对比实验室**(规划中):对模型、Prompt、角色和工作流配置执行批量对比,汇总质量、耗时、Token、费用、失败率和成果差异。
- [ ] **时态记忆与事实冲突检测**(规划中):为记忆和知识图谱增加有效期、当前事实、过期与矛盾检测、事实核验及证据回溯。
@@ -98,9 +104,12 @@
### 开源、构建与发布
- 当前源码候选为 Desktop `0.12.8`、Agent `0.11.21`,OpenCode 固定为 `1.18.29`;正式发布状态以 Desktop 与 Agent 的独立发布渠道为准。
- [x] **0BSD 开源许可**:原创代码可自由使用、复制、修改、分发和商用;第三方组件和资源仍遵循各自许可证。
- [x] **可复现依赖安装与源码构建**:使用锁定依赖、Node.js 24 和统一的测试、类型检查、Lint、生产构建命令。
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希;Linux 同时生成 AppImage、DEB 和 RPM。
- [x] **六平台原生发布矩阵**:Windows、macOS、Linux 的 `x64`、`arm64` 目标由原生 Runner 构建,并提供发布清单和 SHA-256 哈希。Windows 提供 NSIS 与 portable ZIP,macOS 仅 DMG,Linux 提供 AppImage、DEB 和 RPM,共 12 个安装包、20 个发布资产。
- [x] **桌面 Runtime 包校验**:OpenCode 离线依赖复制避开 electron-builder 对源根目录 node_modules 的排除,DSH 会话投影组件打入解包后的 Host。最终包逐文件核对离线依赖并拒绝外部 DeepSeek 导入;Windows x64 CI 还会导入包内 OpenCode 插件、验证真实 DSH UtilityProcess 握手,通过后才发布。
- [x] **DMG-only 更新清单**:当前 Desktop 和官网读取器同时接受仅 DMG 与历史 DMG/ZIP 清单。旧版 macOS 的 GitHub 更新检查及所有旧版镜像更新检查需要从下载页手动升级。
### 开放接口、团队协作与远程执行
+13 -5
View File
@@ -309,7 +309,9 @@
接受用户本机选择的 PNG、JPEG 或 WebP 内联数据,宽高不超过 `4096 × 4096`、总像素不
超过 1600 万且大小不超过 `512 KB`;不得读取远程 Logo URL。名称或副标题溢出品牌区时
单行截断,不能撑宽或挤压侧栏。
- 账户与设置入口固定在侧栏底部。已有稳定设置入口时,不在顶栏重复提供同一入口。
- 侧栏底部固定显示文件、终端、浏览器和资源监控四个系统工具图标,设置按钮独立靠右。工具
图标只负责打开或聚焦助手工作栏面板,不在侧栏复制工具内容。已有稳定设置入口时,不在顶栏
重复提供同一入口。
### 6.9 助手工作栏
@@ -321,10 +323,13 @@
- 升级后默认打开任务中心、工作区、浏览器和成果四个单实例应用,不根据当前页面或 Runtime
能力自动增删稳定应用目录入口。
- 资源监控作为本机单实例应用保留在能力目录,但不加入升级后的默认打开实例。面板只在可见
活动时采样,首期不持久化历史或提供后台负载告警。
- Tab 行只显示已打开实例,行末始终提供“+”普通按钮;点击后在内容区打开应用选择页。
“+”不使用 `tab` 语义,也不能因为终端或其他应用已经打开而消失。
- 应用注册表声明单实例或多实例。单实例重复选择时聚焦已有 Tab;终端从首期开始支持
多实例,每次选择创建独立 Tab 和 Shell Session。
- 应用注册表声明单实例或多实例。在工作栏“+”目录中,单实例重复选择时聚焦已有 Tab;终端
从首期开始支持多实例,每次选择创建独立 Tab 和 Shell Session。主侧栏系统工具快捷入口
可以聚焦当前执行空间中最近使用的终端,额外终端仍从“+”目录创建。
- Task Center 是 Task 的单例应用级索引,不使用“跟随 / 固定目标”多实例模式。每个 Task
只关联一条 Conversation,一条 Conversation 可以关联多个 Task;列表不得复制会话内容,
也不得把 Job/Run 提升为可导航 UI 对象。
@@ -609,6 +614,9 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- “Enter 发送 · Shift+Enter 换行 · Ctrl+V 粘贴图片或文本”等输入操作提示放在空输入框内部,作为主占位文案的次级行;不得在输入框下方单独占用第二行。输入框下方只保留一行当前模式、安全边界或全局快捷键说明。
- 输入操作提示不能替代表单的可访问名称,输入框始终保留持久的程序化标签。
- 空对话展示可执行的起始建议,发送失败保留输入并提供重试。
- 消息流的滚动范围必须使用已渲染消息的真实高度。长会话可以按批次只挂载最近消息并由用户
加载更早内容,但不得用固定的消息占位高度替代实际布局,避免推理、工具列表或长正文被
排除在可滚动范围之外。
### 13.2 最近对话
@@ -640,7 +648,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- 状态卡片使用统一状态令牌,不只依赖颜色。
- 保留“运行概览 / 待处理建议 / 报告与记录 / 心跳计划”四个同级页面。
- 智能心跳菜单入口是完整配置的权威位置;任务中心和设置中心不得复制同一 CRUD 表单。
- “未来分区记忆”仅为长期方向,数据、状态和页面尚未设计,不得显示占位入口。
- 分区记忆协同与唤起条件尚未实现,不得显示占位入口或平行的记忆类型。
### 13.5 Task Center
@@ -700,7 +708,7 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证
- “平台功能 / 通用设置”同时提供默认开启的“在会话中渲染 HTML”持久 Switch。说明必须
明确该能力只生成禁用脚本、联网、表单提交和窗口操作的静态预览;保存成功后当前会话立即
更新,保存失败时保留最后确认状态。
- “平台功能”使用共享 `PageTabs` 区分“通用设置”“远程项目(技术预览)”和“魔法笔记”,默认进入通用设置。“远程项目”页将持久 Switch 与用户数据目录中的 Linux x64/arm64 Agent 包清单分成独立卡片;清单延迟到首次打开该页签时由 Main 完整校验,并读取所选更新源的小型签名目录,显示本地版本、在线最新兼容版本以及“有更新”“已是最新”“未下载”“校验失败”。打开页签或刷新不得自动下载 `.gbagent`;只有用户点击带目标版本的下载/更新按钮后才传输包。签名目录不可用时继续显示并允许导出已验证本地包,提供就地重试错误;清单不混入 Host 已安装状态、缓存路径或敏感签名细节。下载和导入期间保留上下文进度,成功进入全局通知视口。“魔法笔记”页提供默认开启的“显示未完成待办数量”持久 Switch,关闭后只隐藏左侧数量徽标,不改变待办数据或魔法笔记入口。全局模型下载源使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片;选中状态同时依靠 Radio、边框和背景表达,读取失败时不得用默认值伪装为已保存选择。
- “平台功能”使用共享 `PageTabs` 区分“通用设置”“远程项目(技术预览)”和“魔法笔记”,默认进入通用设置。“远程项目”页将持久 Switch 与用户数据目录中的 Linux x64、Linux arm64、macOS arm64 Agent 包清单分成独立卡片;每张卡片与下载进度按平台和架构区分,不能把两个 arm64 目标合并。清单延迟到首次打开该页签时由 Main 完整校验,并读取所选更新源的小型签名目录,显示本地版本、在线最新兼容版本以及“有更新”“已是最新”“未下载”“校验失败”。打开页签或刷新不得自动下载 `.gbagent`;只有用户点击带目标版本的下载/更新按钮后才传输包。签名目录不可用时继续显示并允许导出已验证本地包,提供就地重试错误;清单不混入 Host 已安装状态、缓存路径或敏感签名细节。下载和导入期间保留上下文进度,成功进入全局通知视口。“魔法笔记”页提供默认开启的“显示未完成待办数量”持久 Switch,关闭后只隐藏左侧数量徽标,不改变待办数据或魔法笔记入口。全局模型下载源使用 `fieldset`、持久 `legend` 与整行可点击的原生 Radio 卡片;选中状态同时依靠 Radio、边框和背景表达,读取失败时不得用默认值伪装为已保存选择。
- “关于与更新”的更新源位于“启动时检查新版本”开关下方,常规宽度下将标签、原生单选下拉框和用途说明放在同一行,并复用设置表单的统一控件样式;关闭启动检查后仍可选择更新源,手动检查继续使用当前选择。选项显示“GitHub(默认)”和中性的“镜像节点”。该选择同时控制手动检查、启动时检查、下载页和 Agent 包在线下载,不显示底层服务商名称。
- “关于与更新”在版本信息之后提供应用内反馈卡片。反馈对话框使用原生字段、明确隐私说明和一张可选截图;另提供默认关闭的原生 Checkbox,让用户主动附加最近桌面诊断摘要。Checkbox 附近必须持续说明摘要只含时间、组件、阶段、稳定错误码、错误类型和固定短消息,不含对话、Prompt、凭据、文件内容、路径、Provider 原始响应或远端 Agent 日志。勾选时为诊断摘要预留描述字符预算,超长草稿必须保留并显示字段错误,不得静默截断。失败时保留草稿、勾选状态和请求编号并提供就地重试,成功时在对话框内保留可复制编号而不重复发送全局通知;对话框遵循统一 Modal 的背景隔离、初始焦点、Tab 循环、Escape、焦点恢复和窄窗口布局。
- Agent Runtime 分类页头的“保存设置”同时保存 Runtime 基础配置与 Runtime 原生定制,不在原生定制卡片内提供第二个保存入口。原生定制存在未保存更改时持续显示状态和撤销入口;切换设置分类或 Runtime 不丢弃草稿,关闭设置中心前必须先保存或撤销。
+7 -1
View File
@@ -1,6 +1,6 @@
{
"formatVersion": 1,
"agentVersion": "0.11.18",
"agentVersion": "0.11.21",
"protocol": {
"major": 2,
"minor": 0
@@ -15,6 +15,12 @@
"binaryPath": "node-v24.19.0-linux-x64/bin/node",
"licensePath": "node-v24.19.0-linux-x64/LICENSE"
},
"darwin-arm64": {
"archive": "node-v24.19.0-darwin-arm64.tar.gz",
"sha256": "8294b7aa9b03997481c06babf1e8b270c859358f27da57a11509afe537ac381d",
"binaryPath": "node-v24.19.0-darwin-arm64/bin/node",
"licensePath": "node-v24.19.0-darwin-arm64/LICENSE"
},
"linux-arm64": {
"archive": "node-v24.19.0-linux-arm64.tar.gz",
"sha256": "d28c8a5bf0a808f0ed434a1dce8c54ae98f0371c0bd86ac58abc613f73e6643f",
+33
View File
@@ -0,0 +1,33 @@
const { readFileSync } = require('node:fs')
const {
detectBinaryArchitecture,
detectElfArchitecture
} = require('./binary-architecture.cjs')
function targetName(architecture, platform = 'linux') {
if (
!(platform === 'linux' && ['x64', 'arm64'].includes(architecture)) &&
!(platform === 'darwin' && architecture === 'arm64')
) {
throw new Error(`Unsupported Agent build target: ${platform}-${architecture}`)
}
return `${platform}-${architecture}`
}
function assertTargetBinary(filePath, architecture, platform, label) {
targetName(architecture, platform)
const header = readFileSync(filePath).subarray(0, 64)
const isMachO = header.length >= 8 &&
(header.readUInt32LE(0) === 0xfeedfacf ||
header.readUInt32BE(0) === 0xfeedfacf)
const actual = platform === 'linux'
? detectElfArchitecture(header)
: isMachO ? detectBinaryArchitecture(header) : undefined
if (actual !== architecture) {
throw new Error(
`${label} architecture mismatch: expected ${platform}-${architecture}, received ${actual ?? 'unknown'}`
)
}
}
module.exports = { targetName, assertTargetBinary }
+55 -48
View File
@@ -29,9 +29,11 @@ const {
sep
} = require('node:path')
const tar = require('tar')
const { detectElfArchitecture } = require('./binary-architecture.cjs')
const {
detectElfArchitecture
} = require('./binary-architecture.cjs')
targetName,
assertTargetBinary
} = require('./agent-build-target.cjs')
const {
preflightRegisteredProductionKey
} = require('./signing-key-preflight.cjs')
@@ -132,15 +134,8 @@ function readTrustedKeyRegistry(projectRoot = root) {
return registry
}
function targetName(arch) {
if (!supportedArchitectures.includes(arch)) {
throw new Error(`Unsupported Agent architecture: ${arch}`)
}
return `linux-${arch}`
}
function lockedRuntimeInput(lock, arch, archivePath) {
const target = targetName(arch)
function lockedRuntimeInput(lock, arch, archivePath, platform = 'linux') {
const target = targetName(arch, platform)
const input = lock.node?.targets?.[target]
if (
!input ||
@@ -177,24 +172,23 @@ function lockedRuntimeInput(lock, arch, archivePath) {
function assertElfArchitecture(
filePath,
expectedArchitecture,
description = 'Agent Node runtime'
description = 'Agent Node runtime',
platform = 'linux'
) {
const actual = detectElfArchitecture(readFileSync(filePath).subarray(0, 64))
if (actual !== expectedArchitecture) {
throw new Error(
`${description} architecture mismatch: expected ${expectedArchitecture}, received ${actual ?? 'unknown'}`
)
}
assertTargetBinary(filePath, expectedArchitecture, platform, description)
}
function koffiNativePackageName(arch) {
return `@koromix/koffi-linux-${arch}`
function koffiNativePackageName(arch, platform = 'linux') {
return `@koromix/koffi-${targetName(arch, platform)}`
}
function koffiPayloadPaths(arch) {
function koffiPayloadPaths(arch, platform = 'linux') {
const packageRoot = `lib/node_modules/${koffiPackageName}`
const nativeRoot =
`lib/node_modules/${koffiNativePackageName(arch)}`
`lib/node_modules/${koffiNativePackageName(arch, platform)}`
const native = platform === 'linux'
? [`${nativeRoot}/linux_${arch}/koffi.node`, `${nativeRoot}/musl_${arch}/koffi.node`]
: [`${nativeRoot}/darwin_${arch}/koffi.node`]
return {
packageRoot,
nativeRoot,
@@ -205,13 +199,9 @@ function koffiPayloadPaths(arch) {
`${packageRoot}/src/koffi/src/static.js`,
`${nativeRoot}/package.json`,
`${nativeRoot}/index.js`,
`${nativeRoot}/linux_${arch}/koffi.node`,
`${nativeRoot}/musl_${arch}/koffi.node`
...native
],
native: [
`${nativeRoot}/linux_${arch}/koffi.node`,
`${nativeRoot}/musl_${arch}/koffi.node`
]
native
}
}
@@ -359,7 +349,7 @@ function createManifest(bundleDirectory, metadata) {
formatVersion: 1,
product: 'GoodBuddy',
agentVersion: metadata.agentVersion,
platform: 'linux',
platform: metadata.platform ?? 'linux',
arch: metadata.arch,
protocol: metadata.protocol,
signingKeyId: metadata.signingKeyId,
@@ -452,7 +442,10 @@ function validateManifestShape(manifest, expected) {
manifest.product !== 'GoodBuddy' ||
typeof manifest.agentVersion !== 'string' ||
!semanticVersionPattern.test(manifest.agentVersion) ||
manifest.platform !== 'linux' ||
!(
manifest.platform === 'linux' ||
(manifest.platform === 'darwin' && manifest.arch === 'arm64')
) ||
!supportedArchitectures.includes(manifest.arch) ||
!Number.isSafeInteger(manifest.protocol?.major) ||
!Number.isSafeInteger(manifest.protocol?.minor) ||
@@ -617,7 +610,7 @@ function verifyBundleDirectory(bundleDirectory, options = {}) {
throw new Error(`Agent entrypoint payload is missing: ${required}`)
}
}
const koffiPaths = koffiPayloadPaths(manifest.arch)
const koffiPaths = koffiPayloadPaths(manifest.arch, manifest.platform)
for (const required of koffiPaths.required) {
if (!declaredPaths.has(required)) {
throw new Error(`Agent Koffi payload is missing: ${required}`)
@@ -626,7 +619,7 @@ function verifyBundleDirectory(bundleDirectory, options = {}) {
const koffiLicense = manifest.licenses.find(
(license) => license.package === koffiPackageName
)
const nativePackage = koffiNativePackageName(manifest.arch)
const nativePackage = koffiNativePackageName(manifest.arch, manifest.platform)
const nativeLicense = manifest.licenses.find(
(license) => license.package === nativePackage
)
@@ -641,13 +634,16 @@ function verifyBundleDirectory(bundleDirectory, options = {}) {
}
assertElfArchitecture(
join(bundleDirectory, manifest.entrypoint.runtimePath),
manifest.arch
manifest.arch,
'Agent Node runtime',
manifest.platform
)
for (const nativePath of koffiPaths.native) {
assertElfArchitecture(
join(bundleDirectory, ...nativePath.split('/')),
manifest.arch,
'Agent Koffi native binding'
'Agent Koffi native binding',
manifest.platform
)
}
return {
@@ -668,7 +664,8 @@ function assertReplaceableBundle(directory) {
if (
manifest.formatVersion === 1 &&
manifest.product === 'GoodBuddy' &&
manifest.platform === 'linux'
(manifest.platform === 'linux' ||
(manifest.platform === 'darwin' && manifest.arch === 'arm64'))
) {
return
}
@@ -717,13 +714,13 @@ function copyRegularFile(source, destination) {
chmodSync(destination, 0o644)
}
function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion) {
function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion, platform = 'linux') {
const sourceRoot = join(projectRoot, 'node_modules', koffiPackageName)
const packageMetadata = readJson(
join(sourceRoot, 'package.json'),
'Koffi package metadata'
)
const nativePackage = koffiNativePackageName(arch)
const nativePackage = koffiNativePackageName(arch, platform)
const nativeSourceRoot = join(
projectRoot,
'node_modules',
@@ -739,7 +736,7 @@ function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion) {
nativeMetadata.name !== nativePackage ||
nativeMetadata.version !== lockedVersion ||
!Array.isArray(nativeMetadata.os) ||
!nativeMetadata.os.includes('linux') ||
!nativeMetadata.os.includes(platform) ||
!Array.isArray(nativeMetadata.cpu) ||
!nativeMetadata.cpu.includes(arch)
) {
@@ -748,10 +745,13 @@ function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion) {
)
}
const paths = koffiPayloadPaths(arch)
const paths = koffiPayloadPaths(arch, platform)
const files = [
['package.json', `${paths.packageRoot}/package.json`],
['index.js', `${paths.packageRoot}/index.js`],
['index.cjs', `${paths.packageRoot}/index.cjs`],
['src/koffi/index.cjs', `${paths.packageRoot}/src/koffi/index.cjs`],
['src/koffi/src/static.cjs', `${paths.packageRoot}/src/koffi/src/static.cjs`],
[
'src/koffi/index.js',
`${paths.packageRoot}/src/koffi/index.js`
@@ -770,8 +770,7 @@ function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion) {
for (const source of [
'package.json',
'index.js',
`linux_${arch}/koffi.node`,
`musl_${arch}/koffi.node`
...paths.native.map((path) => path.slice(paths.nativeRoot.length + 1))
]) {
const destination = `${paths.nativeRoot}/${source}`
copyRegularFile(
@@ -783,7 +782,8 @@ function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion) {
assertElfArchitecture(
join(staging, ...nativePath.split('/')),
arch,
'Agent Koffi native binding'
'Agent Koffi native binding',
platform
)
}
return {
@@ -795,11 +795,13 @@ function copyKoffiRuntime(projectRoot, staging, arch, lockedVersion) {
function buildAgentBundle(options) {
const projectRoot = options.projectRoot ?? root
const arch = options.arch
const platform = options.platform ?? 'linux'
const lock = options.lock ?? readRuntimeLock(projectRoot)
const runtimeInput = lockedRuntimeInput(
lock,
arch,
options.runtimeArchive
options.runtimeArchive,
platform
)
const registry =
options.registry ?? readTrustedKeyRegistry(projectRoot)
@@ -843,13 +845,13 @@ function buildAgentBundle(options) {
const destination =
options.outputDirectory ??
join(projectRoot, '.agent-resources', targetName(arch))
join(projectRoot, '.agent-resources', targetName(arch, platform))
mkdirSync(dirname(destination), { recursive: true })
const staging = mkdtempSync(
join(dirname(destination), `.stage-${targetName(arch)}-`)
join(dirname(destination), `.stage-${targetName(arch, platform)}-`)
)
const runtimeStaging = mkdtempSync(
join(tmpdir(), `goodbuddy-node-${targetName(arch)}-`)
join(tmpdir(), `goodbuddy-node-${targetName(arch, platform)}-`)
)
try {
tar.x({
@@ -872,7 +874,7 @@ function buildAgentBundle(options) {
runtimeStaging,
...runtimeInput.licensePath.split('/')
)
assertElfArchitecture(runtimeBinary, arch)
assertElfArchitecture(runtimeBinary, arch, 'Agent Node runtime', platform)
copyFileSync(runtimeBinary, join(staging, 'node'))
chmodSync(join(staging, 'node'), 0o755)
@@ -912,7 +914,8 @@ function buildAgentBundle(options) {
projectRoot,
staging,
arch,
lock.koffi.version
lock.koffi.version,
platform
)
const launcher = [
'#!/bin/sh',
@@ -980,6 +983,7 @@ function buildAgentBundle(options) {
acpSdkVersion: acpSdkPackage.version,
koffiVersion: koffi.version,
koffiNativePackage: koffi.nativePackage,
platform,
arch,
protocol: lock.protocol,
signingKeyId: signingIdentity.keyId
@@ -1004,6 +1008,7 @@ function buildAgentBundle(options) {
? 'test'
: 'production',
expected: {
platform,
agentVersion: lock.agentVersion,
arch,
protocol: lock.protocol
@@ -1073,6 +1078,7 @@ function verifyLockedBundle(bundleDirectory, arch, options = {}) {
options.registry ?? readTrustedKeyRegistry(projectRoot),
verificationEnvironment: options.verificationEnvironment ?? 'production',
expected: {
platform: options.platform ?? 'linux',
agentVersion: lock.agentVersion,
arch,
protocol: lock.protocol
@@ -1203,6 +1209,7 @@ function main(argv = process.argv.slice(2)) {
}
module.exports = {
koffiPayloadPaths,
assertElfArchitecture,
assertSafeManifestPath,
buildAgentBundle,
+38 -10
View File
@@ -32,6 +32,10 @@ const {
} = require('./zip-central-directory.cjs')
const root = resolve(__dirname, '..')
function supportedTarget(platform, architecture) {
return (platform === 'linux' && ['x64', 'arm64'].includes(architecture)) ||
(platform === 'darwin' && architecture === 'arm64')
}
const catalogName = 'agent-catalog.json'
const catalogSignatureName = 'agent-catalog.sig'
const catalogSignatureDomain = Buffer.from(
@@ -46,6 +50,7 @@ const maximumPackageEntries = 50_002
const maximumPackageEntryBytes = 384 * 1024 * 1024
const maximumExpandedPackageBytes = 1024 * 1024 * 1024
const maximumMetadataBytes = 1024 * 1024
const maximumDescriptorBytes = 4 * 1024 * 1024
const maximumCentralDirectoryBytes = 32 * 1024 * 1024
const maximumCatalogEntries = 200
const windowsReservedNamePattern =
@@ -136,7 +141,7 @@ function assertPackageDescriptor(descriptor) {
!semanticVersionPattern.test(
descriptor.minimumDesktopVersion ?? ''
) ||
descriptor.platform !== 'linux' ||
!supportedTarget(descriptor.platform, descriptor.architecture) ||
!['x64', 'arm64'].includes(descriptor.architecture) ||
typeof descriptor.signingKeyId !== 'string' ||
!/^sha256:[a-f0-9]{64}$/u.test(
@@ -214,7 +219,7 @@ function readPackageMetadata(archivePath, registry) {
)
const expectedArchive =
`goodbuddy-agent-${descriptor.version}` +
`-linux-${descriptor.architecture}.gbagent`
`-${descriptor.platform}-${descriptor.architecture}.gbagent`
if (basename(archive) !== expectedArchive) {
throw new Error(
`Agent package archive must be named ${expectedArchive}`
@@ -408,7 +413,10 @@ function streamPackageMetadata(archivePath) {
entry.name === 'agent-package.json' ||
entry.name === 'agent-package.sig'
const size = entry.recordEnd - entry.dataOffset
if (wanted && size > maximumMetadataBytes) {
const maximum = entry.name === 'agent-package.json'
? maximumDescriptorBytes
: maximumMetadataBytes
if (wanted && size > maximum) {
throw new Error('Agent package metadata exceeds its limit')
}
return {
@@ -566,11 +574,11 @@ function assertCatalog(catalog) {
!semanticVersionPattern.test(
entry.minimumDesktopVersion ?? ''
) ||
entry.platform !== 'linux' ||
!supportedTarget(entry.platform, entry.architecture) ||
!['x64', 'arm64'].includes(entry.architecture) ||
entry.archive !==
`goodbuddy-agent-${entry.version}` +
`-linux-${entry.architecture}.gbagent` ||
`-${entry.platform}-${entry.architecture}.gbagent` ||
!Number.isSafeInteger(entry.size) ||
entry.size <= 0 ||
!sha256Pattern.test(entry.sha256 ?? '') ||
@@ -600,7 +608,7 @@ function assertCatalog(catalog) {
entry.remoteRuntime.protocol,
'Remote Runtime protocol'
)
const identity = `${entry.version}:${entry.architecture}`
const identity = `${entry.version}:${entry.platform}:${entry.architecture}`
if (identities.has(identity)) {
throw new Error('Agent catalog contains duplicate entries')
}
@@ -698,7 +706,8 @@ function createCatalog(options) {
}
const packages = [
readPackageMetadata(options.x64Package, registry),
readPackageMetadata(options.arm64Package, registry)
readPackageMetadata(options.arm64Package, registry),
...(options.darwinArm64Package ? [readPackageMetadata(options.darwinArm64Package, registry)] : [])
]
const [first, second] = packages
const {
@@ -721,12 +730,26 @@ function createCatalog(options) {
canonicalJson(secondRuntimeIdentity) ||
new Set(packages.map(
(metadata) => metadata.descriptor.architecture
)).size !== 2
).slice(0, 2)).size !== 2 ||
first.descriptor.platform !== 'linux' ||
second.descriptor.platform !== 'linux'
) {
throw new Error(
'Agent package matrix does not describe one coherent release'
)
}
if (packages[2]) {
const mac = packages[2].descriptor
const { bundleDigest: _digest, ...runtimeIdentity } = mac.remoteRuntime
void _digest
if (
mac.platform !== 'darwin' || mac.architecture !== 'arm64' ||
mac.version !== first.descriptor.version ||
mac.minimumDesktopVersion !== first.descriptor.minimumDesktopVersion ||
canonicalJson(mac.agentProtocol) !== canonicalJson(first.descriptor.agentProtocol) ||
canonicalJson(runtimeIdentity) !== canonicalJson(firstRuntimeIdentity)
) throw new Error('Agent package matrix does not describe one coherent release')
}
const previous =
options.previousCatalog && options.previousSignature
? readVerifiedCatalog(
@@ -745,11 +768,11 @@ function createCatalog(options) {
}
const entries = new Map()
for (const entry of previous?.entries ?? []) {
entries.set(`${entry.version}:${entry.architecture}`, entry)
entries.set(`${entry.version}:${entry.platform}:${entry.architecture}`, entry)
}
for (const metadata of packages) {
const entry = catalogEntry(metadata)
const identity = `${entry.version}:${entry.architecture}`
const identity = `${entry.version}:${entry.platform}:${entry.architecture}`
const existing = entries.get(identity)
if (
existing &&
@@ -764,6 +787,7 @@ function createCatalog(options) {
const sortedEntries = [...entries.values()].sort(
(left, right) =>
compareVersions(right.version, left.version) ||
left.platform.localeCompare(right.platform, 'en') ||
left.architecture.localeCompare(right.architecture, 'en')
)
if (sortedEntries.length > maximumCatalogEntries) {
@@ -813,6 +837,7 @@ function parseArguments(argv) {
![
'--x64-package',
'--arm64-package',
'--darwin-arm64-package',
'--previous-catalog',
'--previous-signature',
'--catalog',
@@ -852,6 +877,9 @@ function parseArguments(argv) {
command,
x64Package: resolve(options['--x64-package']),
arm64Package: resolve(options['--arm64-package']),
...(options['--darwin-arm64-package'] ? {
darwinArm64Package: resolve(options['--darwin-arm64-package'])
} : {}),
...(options['--previous-catalog']
? {
previousCatalog: resolve(
+50 -3
View File
@@ -24,6 +24,7 @@ const {
assembleAgentPackage
} = require('./agent-package.cjs')
const { sha256File } = require('./file-hash.cjs')
const { targetName } = require('./agent-build-target.cjs')
const ciSigningKeyId = 'goodbuddy-agent-ci-ephemeral'
@@ -34,6 +35,7 @@ function parseArguments(argv) {
if (
![
'--arch',
'--platform',
'--node-archive',
'--opencode-archive',
'--output-directory',
@@ -56,6 +58,8 @@ function parseArguments(argv) {
if (!supportedArchitectures.includes(options.arch)) {
throw new Error('--arch must be x64 or arm64')
}
const platform = options.platform ?? 'linux'
targetName(options.arch, platform)
for (const key of [
'nodearchive',
'opencodearchive',
@@ -70,6 +74,7 @@ function parseArguments(argv) {
}
return {
arch: options.arch,
platform,
nodeArchive: resolve(options.nodearchive),
opencodeArchive: resolve(options.opencodearchive),
outputDirectory: resolve(options.outputdirectory),
@@ -77,6 +82,14 @@ function parseArguments(argv) {
}
}
function readCiLocks(platform = 'linux', architecture = 'x64') {
const projectRoot = resolve(__dirname, '..')
targetName(architecture, platform)
const lock = readRuntimeLock(projectRoot)
const runtimeLock = readRemoteRuntimeLock(projectRoot)
return { lock, runtimeLock }
}
function ephemeralSigningIdentity() {
const { privateKey, publicKey } = generateKeyPairSync('ed25519')
return {
@@ -97,8 +110,12 @@ function ephemeralSigningIdentity() {
async function buildCiAgentBundle(options) {
const projectRoot = resolve(__dirname, '..')
const lock = readRuntimeLock(projectRoot)
const runtimeLock = readRemoteRuntimeLock(projectRoot)
const platform = options.platform ?? 'linux'
const target = targetName(options.arch, platform)
if (process.platform !== platform || process.arch !== options.arch) {
throw new Error(`Agent CI build requires a native ${target} host`)
}
const { lock, runtimeLock } = readCiLocks(platform, options.arch)
const signing = ephemeralSigningIdentity()
const firstArchiveRoot = resolve(
join(
@@ -124,6 +141,8 @@ async function buildCiAgentBundle(options) {
force: true
})
buildAgentBundle({
platform,
lock,
projectRoot,
arch: options.arch,
runtimeArchive: options.nodeArchive,
@@ -132,6 +151,8 @@ async function buildCiAgentBundle(options) {
testSigningIdentity: identity
})
const runtime = buildRuntimeBundle({
platform,
lock: runtimeLock,
projectRoot,
architecture: options.arch,
runtimeArchive: options.opencodeArchive,
@@ -141,6 +162,7 @@ async function buildCiAgentBundle(options) {
})
const assemble = (archive) =>
assembleAgentPackage({
platform,
projectRoot,
architecture: options.arch,
minimumDesktopVersion:
@@ -181,7 +203,31 @@ async function buildCiAgentBundle(options) {
throw new Error('Built Agent native launch smoke failed')
}
const nativeChecks = [
[
resolve(options.outputDirectory, 'node'),
['-e', `const k = require(${JSON.stringify(resolve(options.outputDirectory, 'lib/node_modules/koffi'))}); if (k.sizeof('int') !== 4) process.exit(1)`]
],
[join(runtime.bundleDirectory, 'bin', 'opencode'), ['--version']]
]
for (const [executable, args] of nativeChecks) {
const result = spawnSync(executable, args, {
encoding: 'utf8',
timeout: 30_000,
windowsHide: true
})
if (
result.error ||
result.status !== 0 ||
(args[0] === '--version' &&
result.stdout.trim() !== runtimeLock.runtimes.opencode.version)
) {
throw new Error(`Built native dependency smoke failed: ${basename(executable)}`)
}
}
return {
platform,
agentVersion: lock.agentVersion,
remoteRuntimeVersion:
runtimeLock.runtimes.opencode.version,
@@ -214,5 +260,6 @@ if (require.main === module) {
module.exports = {
buildCiAgentBundle,
parseArguments
parseArguments,
readCiLocks
}
+13 -3
View File
@@ -19,6 +19,7 @@ const {
} = require('node:fs')
const { basename, dirname, join, relative, resolve, sep } = require('node:path')
const { tmpdir } = require('node:os')
const { targetName } = require('./agent-build-target.cjs')
const { crc32 } = require('node:zlib')
const {
buildAgentBundle,
@@ -345,6 +346,8 @@ function createPackageArchive(
function assembleAgentPackage(options) {
const projectRoot = options.projectRoot ?? root
const architecture = options.architecture
const platform = options.platform ?? 'linux'
const target = targetName(architecture, platform)
if (
!['x64', 'arm64'].includes(architecture) ||
!semanticVersionPattern.test(options.minimumDesktopVersion ?? '') ||
@@ -386,6 +389,7 @@ function assembleAgentPackage(options) {
options.agentBundle,
architecture,
{
platform,
projectRoot,
registry,
lock: agentLock,
@@ -395,6 +399,7 @@ function assembleAgentPackage(options) {
}
)
const runtime = verifyRuntimeBundle(options.runtimeBundle, {
platform,
projectRoot,
architecture,
registry,
@@ -405,14 +410,14 @@ function assembleAgentPackage(options) {
})
const expectedArchiveName =
`goodbuddy-agent-${agent.manifest.agentVersion}` +
`-linux-${architecture}.gbagent`
`-${target}.gbagent`
if (basename(options.output) !== expectedArchiveName) {
throw new Error(
`Agent package output must be named ${expectedArchiveName}`
)
}
const staging = mkdtempSync(
join(tmpdir(), `goodbuddy-agent-package-linux-${architecture}-`)
join(tmpdir(), `goodbuddy-agent-package-${target}-`)
)
try {
stagePackagePayload({
@@ -431,7 +436,7 @@ function assembleAgentPackage(options) {
component: 'agent',
version: agent.manifest.agentVersion,
minimumDesktopVersion: options.minimumDesktopVersion,
platform: 'linux',
platform,
architecture,
signingKeyId: signingIdentity.keyId,
agentProtocol: agent.manifest.protocol,
@@ -493,6 +498,7 @@ function buildAgentPackage(options) {
)
try {
const agentBundle = buildAgentBundle({
platform: options.platform ?? 'linux',
projectRoot,
arch: options.architecture,
runtimeArchive: options.nodeArchive,
@@ -504,6 +510,7 @@ function buildAgentPackage(options) {
: { signingIdentity })
})
const runtime = buildRuntimeBundle({
platform: options.platform ?? 'linux',
projectRoot,
architecture: options.architecture,
runtimeArchive: options.runtimeArchive,
@@ -544,6 +551,7 @@ function parseArguments(argv) {
if (
![
'--arch',
'--platform',
'--minimum-desktop-version',
'--node-archive',
'--runtime-archive',
@@ -571,10 +579,12 @@ function parseArguments(argv) {
throw new Error('Agent package arguments are incomplete')
}
const common = {
platform: options.platform ?? 'linux',
architecture: options.arch,
minimumDesktopVersion: options.minimumDesktopVersion,
output: resolve(options.output)
}
targetName(common.architecture, common.platform)
if (command === 'build') {
if (!options.nodeArchive || !options.runtimeArchive) {
throw new Error('Agent package build inputs are incomplete')
+2 -2
View File
@@ -19,8 +19,8 @@ const manifestName = 'release-manifest.json'
const targetDefinitions = [
{ platform: 'windows', arch: 'x64', formats: ['nsis', 'portable'] },
{ platform: 'windows', arch: 'arm64', formats: ['nsis', 'portable'] },
{ platform: 'macos', arch: 'x64', formats: ['dmg', 'zip'] },
{ platform: 'macos', arch: 'arm64', formats: ['dmg', 'zip'] },
{ platform: 'macos', arch: 'x64', formats: ['dmg'] },
{ platform: 'macos', arch: 'arm64', formats: ['dmg'] },
{ platform: 'linux', arch: 'x64', formats: ['AppImage', 'deb', 'rpm'] },
{ platform: 'linux', arch: 'arm64', formats: ['AppImage', 'deb', 'rpm'] }
]
+30 -2
View File
@@ -42,6 +42,10 @@ const {
const {
readZipCentralDirectory
} = require('./zip-central-directory.cjs')
const {
verifyOpenCodeConfig,
verifyHarnessBundleImports
} = require('./verify-desktop-runtimes.cjs')
const root = join(__dirname, '..')
const packageJson = JSON.parse(
@@ -90,6 +94,8 @@ const portableRequiredFiles = [
'resources/tray-icon.png',
'resources/tool-environment/managed-python-artifacts.json',
'resources/runtimes/opencode/opencode.exe',
'resources/runtimes/opencode-config/node_modules/@opencode-ai/plugin/package.json',
'resources/runtimes/opencode-config/node_modules/@opencode-ai/plugin/dist/index.js',
'resources/runtimes/continue/package.json',
'resources/runtimes/npm/bin/npm-cli.js',
'resources/runtimes/npm/bin/npx-cli.js',
@@ -126,7 +132,7 @@ const platformDefinitions = {
},
macos: {
builderFlag: '--mac',
defaultFormats: ['dmg', 'zip'],
defaultFormats: ['dmg'],
supportedFormats: ['dmg', 'zip'],
unpackedPattern: /^mac(?:-.+)?$/u,
executable: [
@@ -820,6 +826,10 @@ function verifyHarnessPackage(
join(unpackedRoot, ...harnessHostEntry.split('/')),
'DeepSeek Harness 可执行 Host'
)
verifyHarnessBundleImports(
readFileSync(join(unpackedRoot, ...harnessHostEntry.split('/')), 'utf8'),
harnessHostEntry
)
const harnessLlmChunk = [...entries]
.map((entry) => entry.slice(1).split(sep).join('/'))
.find((entry) =>
@@ -868,6 +878,10 @@ function verifyHarnessPackage(
join(unpackedRoot, ...chunkPath.split('/')),
'DeepSeek Harness 可执行 module chunk'
)
verifyHarnessBundleImports(
readFileSync(join(unpackedRoot, ...chunkPath.split('/')), 'utf8'),
chunkPath
)
}
for (const [packageName, expectedVersion] of Object.entries(
harnessPackageVersions
@@ -1080,6 +1094,10 @@ function verifyUnpackedOutput(directory, options) {
'Remote Runtime 锁定清单'
)
assertFile(runtimeExecutable, 'OpenCode Runtime')
verifyOpenCodeConfig(
resources,
join(root, '.runtime-resources', 'opencode-config')
)
assertFile(
join(resources, 'runtimes', 'continue', 'dist', 'index.js'),
'Continue Runtime'
@@ -1567,7 +1585,7 @@ function printHelp() {
默认格式:
windows: nsis, portable (ZIP)
macos: dmg, zip
macos: dmg
linux: AppImage, deb, rpm`)
}
@@ -1629,6 +1647,16 @@ async function main(argv = process.argv.slice(2)) {
stagingDirectory,
options
)
if (
process.env.GITHUB_ACTIONS === 'true' &&
options.platform === 'windows' &&
options.arch === 'x64'
) {
await run(process.execPath, [
join(root, 'build', 'run-packaged-deepseek-harness-smoke.cjs'),
unpackedDirectory
])
}
if (
options.platform === 'windows' &&
options.formats.includes('portable')
+1 -1
View File
@@ -184,7 +184,7 @@ async function run() {
model: 'qwen-plus',
supportsImageInput: false,
requestHeaders: {},
harnessVersion: '0.1.0-rc.8',
harnessVersion: '0.1.2-rc.1',
credentialRefs: ['GOODBUDDY_HARNESS_MODEL_API_KEY'],
skillPackages: [],
extensionPackages: [],
+120
View File
@@ -0,0 +1,120 @@
const { existsSync } = require("node:fs");
const {
mkdirSync,
readFileSync,
renameSync,
rmSync,
writeFileSync,
} = require("node:fs");
const { join } = require("node:path");
const { spawnSync } = require("node:child_process");
const opencodeVersion = "1.18.29";
const opencodePluginPackage = "@opencode-ai/plugin";
function npmInvocation() {
const npmCli = process.env.npm_execpath;
if (npmCli) {
return { command: process.execPath, prefixArgs: [npmCli] };
}
if (process.platform === "win32") {
throw new Error("npm_execpath is required to prepare OpenCode config");
}
return { command: "npm", prefixArgs: [] };
}
function prepareBundledOpenCodeConfig(projectDir) {
const targetDirectory = join(
projectDir,
".runtime-resources",
"opencode-config",
);
try {
const installed = JSON.parse(
readFileSync(
join(
targetDirectory,
"node_modules",
"@opencode-ai",
"plugin",
"package.json",
),
"utf8",
),
);
if (installed.version === opencodeVersion) {
return targetDirectory;
}
} catch {
// Rebuild an incomplete or stale offline config template.
}
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`;
rmSync(stagingDirectory, { recursive: true, force: true });
mkdirSync(stagingDirectory, { recursive: true });
try {
writeFileSync(
join(stagingDirectory, "package.json"),
`${JSON.stringify(
{
private: true,
dependencies: {
[opencodePluginPackage]: opencodeVersion,
},
},
null,
2,
)}\n`,
"utf8",
);
writeFileSync(join(stagingDirectory, ".gitignore"), "node_modules\n");
const npm = npmInvocation();
const environment = { ...process.env };
delete environment.NODE_TLS_REJECT_UNAUTHORIZED;
const result = spawnSync(
npm.command,
[
...npm.prefixArgs,
"install",
"--ignore-scripts",
"--bin-links=false",
"--omit=dev",
"--omit=optional",
"--no-audit",
"--no-fund",
"--save-exact",
],
{
cwd: stagingDirectory,
encoding: "utf8",
env: environment,
maxBuffer: 16 * 1024 * 1024,
shell: false,
windowsHide: true,
},
);
if (result.status !== 0) {
throw new Error(
`Unable to prepare bundled OpenCode config: ${
result.error?.message ||
result.stderr ||
result.stdout ||
`npm exited with ${result.status}`
}`,
);
}
rmSync(targetDirectory, { recursive: true, force: true });
mkdirSync(join(targetDirectory, ".."), { recursive: true });
renameSync(stagingDirectory, targetDirectory);
return targetDirectory;
} finally {
if (existsSync(stagingDirectory)) {
rmSync(stagingDirectory, { recursive: true, force: true });
}
}
}
module.exports = {
opencodeVersion,
prepareBundledOpenCodeConfig,
};
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -88,7 +88,7 @@ async function main() {
`${JSON.stringify(
{
name: '@deepseek-ai/dsh-llm',
version: '0.1.0-rc.8',
version: '0.1.2-rc.1',
private: true,
type: 'module'
},
@@ -13,6 +13,7 @@ const {
const { statSync } = require('node:fs')
const { tmpdir } = require('node:os')
const { delimiter, join, resolve } = require('node:path')
const { pathToFileURL } = require('node:url')
const unpackedPath = process.argv[2]
? resolve(process.argv[2])
@@ -109,6 +110,20 @@ async function main() {
const npmProject = join(root, 'npm-project')
const npmFixture = join(root, 'npm-fixture')
await mkdir(project, { recursive: true })
const pluginUrl = pathToFileURL(join(
unpackedPath, 'resources', 'runtimes', 'opencode-config',
'node_modules', '@opencode-ai', 'plugin', 'dist', 'index.js'
)).href
const importedPlugin = await run(
executable,
['--input-type=module', '--eval', `await import(${JSON.stringify(pluginUrl)})`],
{ ...process.env, ELECTRON_RUN_AS_NODE: '1' },
root
)
if (importedPlugin.exitCode !== 0 || importedPlugin.signal) {
throw new Error(`Packaged OpenCode plugin import failed: ${importedPlugin.output.trim()}`)
}
console.log('Packaged OpenCode offline plugin import: ready')
await copyFile(
resolve('build/deepseek-harness-utility-smoke.cjs'),
+110 -119
View File
@@ -1,4 +1,4 @@
const { createHash } = require('node:crypto')
const { createHash } = require("node:crypto");
const {
chmod,
mkdir,
@@ -6,101 +6,100 @@ const {
rename,
rm,
stat,
writeFile
} = require('node:fs/promises')
const { existsSync } = require('node:fs')
const { join } = require('node:path')
const { spawnSync } = require('node:child_process')
const tar = require('tar')
const { sha256File } = require('./file-hash.cjs')
writeFile,
} = require("node:fs/promises");
const { existsSync } = require("node:fs");
const { join } = require("node:path");
const { spawnSync } = require("node:child_process");
const tar = require("tar");
const { sha256File } = require("./file-hash.cjs");
const {
opencodeVersion,
prepareBundledOpenCodeConfig,
} = require("./opencode-config.cjs");
const opencodeVersion = '1.18.9'
const architectureNames = {
1: 'x64',
3: 'arm64'
}
1: "x64",
3: "arm64",
};
const platformNames = {
darwin: 'darwin',
linux: 'linux',
win32: 'windows'
}
darwin: "darwin",
linux: "linux",
win32: "windows",
};
function sha512Integrity(contents) {
return `sha512-${createHash('sha512').update(contents).digest('base64')}`
return `sha512-${createHash("sha512").update(contents).digest("base64")}`;
}
async function lockedIntegrity(projectDir, packageName) {
const lock = JSON.parse(
await readFile(join(projectDir, 'package-lock.json'), 'utf8')
)
const entry = lock.packages?.[`node_modules/${packageName}`]
await readFile(join(projectDir, "package-lock.json"), "utf8"),
);
const entry = lock.packages?.[`node_modules/${packageName}`];
if (
entry?.version !== opencodeVersion ||
typeof entry.integrity !== 'string'
typeof entry.integrity !== "string"
) {
throw new Error(
`Missing locked ${packageName}@${opencodeVersion} integrity`
)
`Missing locked ${packageName}@${opencodeVersion} integrity`,
);
}
return entry.integrity
return entry.integrity;
}
function npmInvocation() {
const npmCli = process.env.npm_execpath
const npmCli = process.env.npm_execpath;
if (npmCli) {
return {
command: process.execPath,
prefixArgs: [npmCli]
}
prefixArgs: [npmCli],
};
}
if (process.platform === 'win32') {
throw new Error('npm_execpath is required to prepare bundled runtimes')
if (process.platform === "win32") {
throw new Error("npm_execpath is required to prepare bundled runtimes");
}
return {
command: 'npm',
prefixArgs: []
}
command: "npm",
prefixArgs: [],
};
}
async function downloadPackage(projectDir, packageName, integrity) {
const cacheDirectory = join(
projectDir,
'.runtime-resources',
'cache'
)
await mkdir(cacheDirectory, { recursive: true })
const cacheDirectory = join(projectDir, ".runtime-resources", "cache");
await mkdir(cacheDirectory, { recursive: true });
const archivePath = join(
cacheDirectory,
`${packageName}-${opencodeVersion}.tgz`
)
`${packageName}-${opencodeVersion}.tgz`,
);
if (existsSync(archivePath)) {
const cached = await readFile(archivePath)
const cached = await readFile(archivePath);
if (sha512Integrity(cached) === integrity) {
return archivePath
return archivePath;
}
await rm(archivePath, { force: true })
await rm(archivePath, { force: true });
}
const npm = npmInvocation()
const npm = npmInvocation();
const result = spawnSync(
npm.command,
[
...npm.prefixArgs,
'pack',
"pack",
`${packageName}@${opencodeVersion}`,
'--ignore-scripts',
'--json',
'--pack-destination',
cacheDirectory
"--ignore-scripts",
"--json",
"--pack-destination",
cacheDirectory,
],
{
cwd: projectDir,
encoding: 'utf8',
encoding: "utf8",
maxBuffer: 16 * 1024 * 1024,
shell: false,
windowsHide: true
}
)
windowsHide: true,
},
);
if (result.status !== 0) {
throw new Error(
`Unable to fetch ${packageName}: ${
@@ -108,115 +107,107 @@ async function downloadPackage(projectDir, packageName, integrity) {
result.stderr ||
result.stdout ||
`npm exited with ${result.status}`
}`
)
}`,
);
}
const output = JSON.parse(result.stdout)
const downloadedPath = join(cacheDirectory, output[0].filename)
const contents = await readFile(downloadedPath)
const output = JSON.parse(result.stdout);
const downloadedPath = join(cacheDirectory, output[0].filename);
const contents = await readFile(downloadedPath);
if (sha512Integrity(contents) !== integrity) {
await rm(downloadedPath, { force: true })
throw new Error(`Integrity verification failed for ${packageName}`)
await rm(downloadedPath, { force: true });
throw new Error(`Integrity verification failed for ${packageName}`);
}
if (downloadedPath !== archivePath) {
await rm(archivePath, { force: true })
await rename(downloadedPath, archivePath)
await rm(archivePath, { force: true });
await rename(downloadedPath, archivePath);
}
return archivePath
return archivePath;
}
module.exports = async function prepareBundledRuntimes(context) {
const platform = context.electronPlatformName
const architecture = architectureNames[context.arch]
const packagePlatform = platformNames[platform]
const platform = context.electronPlatformName;
const architecture = architectureNames[context.arch];
const packagePlatform = platformNames[platform];
if (!architecture || !packagePlatform) {
throw new Error(
`Bundled OpenCode does not support ${platform}/${context.arch}`
)
`Bundled OpenCode does not support ${platform}/${context.arch}`,
);
}
const suffix =
architecture === 'x64' ? `${architecture}-baseline` : architecture
const packageName = `opencode-${packagePlatform}-${suffix}`
const projectDir = context.packager.projectDir
architecture === "x64" ? `${architecture}-baseline` : architecture;
const packageName = `opencode-${packagePlatform}-${suffix}`;
const projectDir = context.packager.projectDir;
await prepareBundledOpenCodeConfig(projectDir);
const projectPackage = JSON.parse(
await readFile(join(projectDir, 'package.json'), 'utf8')
)
await readFile(join(projectDir, "package.json"), "utf8"),
);
await writeFile(
join(projectDir, 'out', 'main', 'package.json'),
join(projectDir, "out", "main", "package.json"),
`${JSON.stringify(
{
name: '@deepseek-ai/dsh-llm',
version:
projectPackage.dependencies['@deepseek-ai/dsh-llm'],
name: "@deepseek-ai/dsh-llm",
version: projectPackage.dependencies["@deepseek-ai/dsh-llm"],
private: true,
type: 'module'
type: "module",
},
null,
2
2,
)}\n`,
'utf8'
)
const integrity = await lockedIntegrity(projectDir, packageName)
const targetDirectory = join(
projectDir,
'.runtime-resources',
architecture
)
const readyPath = join(targetDirectory, '.ready.json')
const executable = platform === 'win32' ? 'opencode.exe' : 'opencode'
const preparedPath = join(targetDirectory, executable)
"utf8",
);
const integrity = await lockedIntegrity(projectDir, packageName);
const targetDirectory = join(projectDir, ".runtime-resources", architecture);
const readyPath = join(targetDirectory, ".ready.json");
const executable = platform === "win32" ? "opencode.exe" : "opencode";
const preparedPath = join(targetDirectory, executable);
const identity = {
packageName,
version: opencodeVersion,
integrity
}
integrity,
};
try {
const ready = JSON.parse(await readFile(readyPath, 'utf8'))
const ready = JSON.parse(await readFile(readyPath, "utf8"));
if (
ready.packageName === identity.packageName &&
ready.version === identity.version &&
ready.integrity === identity.integrity &&
typeof ready.executableSha256 === 'string' &&
(platform === 'win32' ||
typeof ready.executableSha256 === "string" &&
(platform === "win32" ||
((await stat(preparedPath)).mode & 0o111) !== 0) &&
(await sha256File(preparedPath)) === ready.executableSha256
) {
return
return;
}
} catch {
// Rebuild an incomplete or stale runtime cache.
}
const archivePath = await downloadPackage(
projectDir,
packageName,
integrity
)
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`
await rm(stagingDirectory, { recursive: true, force: true })
await mkdir(stagingDirectory, { recursive: true })
const archivePath = await downloadPackage(projectDir, packageName, integrity);
const stagingDirectory = `${targetDirectory}.staging-${process.pid}`;
await rm(stagingDirectory, { recursive: true, force: true });
await mkdir(stagingDirectory, { recursive: true });
try {
await tar.x({
file: archivePath,
cwd: stagingDirectory,
strip: 1
})
const sourcePath = join(stagingDirectory, 'bin', executable)
const stagingExecutable = join(stagingDirectory, executable)
await rename(sourcePath, stagingExecutable)
if (platform !== 'win32') {
await chmod(stagingExecutable, 0o755)
strip: 1,
});
const sourcePath = join(stagingDirectory, "bin", executable);
const stagingExecutable = join(stagingDirectory, executable);
await rename(sourcePath, stagingExecutable);
if (platform !== "win32") {
await chmod(stagingExecutable, 0o755);
}
const executableSha256 = await sha256File(stagingExecutable)
const executableSha256 = await sha256File(stagingExecutable);
await writeFile(
join(stagingDirectory, '.ready.json'),
join(stagingDirectory, ".ready.json"),
JSON.stringify({ ...identity, executableSha256 }),
'utf8'
)
await rm(targetDirectory, { recursive: true, force: true })
await rename(stagingDirectory, targetDirectory)
"utf8",
);
await rm(targetDirectory, { recursive: true, force: true });
await rename(stagingDirectory, targetDirectory);
} finally {
await rm(stagingDirectory, { recursive: true, force: true })
await rm(stagingDirectory, { recursive: true, force: true });
}
}
};
+39
View File
@@ -0,0 +1,39 @@
const { readdirSync, readFileSync, statSync } = require('node:fs')
const { join, relative } = require('node:path')
const { opencodeVersion } = require('./opencode-config.cjs')
function verifyOpenCodeConfig(resources, source) {
const destination = join(resources, 'runtimes', 'opencode-config')
const manifestPath = join(destination, 'node_modules', '@opencode-ai', 'plugin', 'package.json')
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
if (manifest.name !== '@opencode-ai/plugin' || manifest.version !== opencodeVersion) {
throw new Error('Packaged OpenCode plugin version is invalid')
}
const pending = [source]
let files = 0
while (pending.length > 0) {
const directory = pending.pop()
for (const entry of readdirSync(directory, { withFileTypes: true })) {
const file = join(directory, entry.name)
if (entry.isDirectory()) {
pending.push(file)
} else if (entry.isFile()) {
const target = join(destination, relative(source, file))
if (!statSync(target, { throwIfNoEntry: false })?.isFile() ||
!readFileSync(target).equals(readFileSync(file))) {
throw new Error(`Packaged OpenCode dependency is missing or changed: ${relative(source, file)}`)
}
files++
}
}
}
return files
}
function verifyHarnessBundleImports(source, name) {
if (/(?:\bfrom\s*|\bimport\s*(?:\(\s*)?|\brequire\s*\(\s*)["']@deepseek-ai\//u.test(source)) {
throw new Error(`DeepSeek Harness bundle contains an external DeepSeek dependency: ${name}`)
}
}
module.exports = { verifyOpenCodeConfig, verifyHarnessBundleImports }
@@ -37,11 +37,11 @@ GoodBuddy 当前已经具备若干长期助手能力,但它们仍是彼此分
## 2. 核心产品判断
### 2.1 智能心跳保持独立,未来分区记忆另行设计
### 2.1 智能心跳保持独立,分区记忆协同另行设计
当前智能心跳继续承担周期回顾、报告和建议,并支持 Global 或指定 Project 范围。它不是
Task、通用调度器或后台 Agent,也不进入统一 `AutomationPlan.kind`。未来分区记忆的
数据、状态、唤起和页面需要独立设计,不能从当前方向直接推导。
Task、通用调度器或后台 Agent,也不进入统一 `AutomationPlan.kind`。按分区回顾、候选生成
以及时间/内部事件唤起需要独立设计,不能从当前心跳行为直接推导,也不创建平行记忆类型。
- Scheduled Task 解决“何时在一个 Task 中执行新的 Job”。
- Goal Task 解决“围绕结果在同一 Task 中持续规划和推进”。
@@ -507,7 +507,7 @@ experiment_runs
现有 `schedules`、`schedule_runs`、`heartbeat_configs`、`heartbeat_runs`、
`heartbeat_entries`、`tasks` 和 `runs` 不应一次性重写。Schedule 可渐进建立稳定 Task 与
Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心跳数据保持独立,不得
静默转成 `AutomationPlan` 或顶层 Task。未来分区记忆完成设计前,不新增迁移目标。
静默转成 `AutomationPlan` 或顶层 Task。分区记忆协同和唤起条件完成设计前,不新增迁移目标。
## 16. 分阶段实施
@@ -518,7 +518,7 @@ Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心
- 明确 Task N:1 Conversation 关系、左侧行首展开按钮与 Task 子项图标,以及 Task Center
索引边界,不复制内容。
- 为现有 Scheduled Task 和专家执行建立按 Task 聚合的活动视图。
- 明确当前心跳保持独立,未来分区记忆尚待设计。
- 明确当前心跳保持独立,分区记忆协同与唤起条件尚待设计。
- 补充触发来源、运行版本、预算和读写范围展示。
### 阶段 1:调度与运行基础
@@ -569,7 +569,7 @@ Conversation 关联,旧 child-task 字段可兼容映射到 Job/Subjob;心
## 18. 总体验收标准
- [ ] 智能心跳保持独立,不作为 Task 类型;未来分区记忆尚未设计。
- [ ] 智能心跳保持独立,不作为 Task 类型;分区记忆协同与唤起条件尚未设计。
- [ ] Task Center 只索引 Task;每个 Task 只关联一条 Conversation,一条 Conversation 可以
关联多个 Task。
- [ ] 当前 UI 只展示到 Task,不提供 Job/Subjob/Run 树或独立导航。
+24 -17
View File
@@ -9,7 +9,7 @@
| 日期 | 2026-09-03 |
| 适用范围 | GoodBuddy 桌面端、ShareServer、网关间联邦协议 |
| 桌面端设计 | [设备发现与共享](../features/device-sharing/README.md) |
| 服务端设计 | [ShareServer](../features/share-server/README.md) |
| 服务端设计 | [ShareServer](../../../goodbuddy-shareserver/docs/features/share-server/README.md) |
## 1. 文档职责
@@ -72,7 +72,8 @@ ShareServer 负责身份、目录、策略、任务路由、更新和审计。
| `Package` | 可下载到本机安装的签名能力工件 |
| `FederationTrust` | 两个组织网关之间经管理员确认的双边信任关系 |
“网关”是 ShareServer 的网络角色;产品名称和代码目录统一使用 `ShareServer` / `shareserver`。
“网关”是 ShareServer 的网络角色;产品名称为 `ShareServer`,独立仓库为
`goodbuddy-shareserver`。
## 4. 系统拓扑
@@ -406,21 +407,27 @@ Revocation
## 15. 仓库边界
目标目录结构:
当前仓库边界(跨仓库文档链接约定两个仓库克隆到同一级目录):
```text
docs/
architecture/share-network-architecture.md
features/device-sharing/
features/share-server/
packages/
share-protocol/ # 与 Electron、数据库和 Web 框架无关的 schema/类型
shareserver/ # 独立 Web 服务及管理前端
src/ # GoodBuddy Desktop
goodbuddy/
docs/architecture/share-network-architecture.md
docs/features/device-sharing/
src/ # GoodBuddy Desktop
goodbuddy-shareserver/
docs/features/share-server/
server/ # 独立 Hono Web 服务
shared/ # 当前原型 fixture,不是跨端协议
src/ # React 管理前端
```
当前文档先固定边界,不创建空服务脚手架。实现时 `shareserver` 不反向依赖桌面端 `src/shared`;
双方依赖独立 `share-protocol`。协议包不得包含数据库模型、Electron API、服务实现或凭据。
ShareServer 源码和服务端文档已迁入独立仓库:
`ssh://git@git.digiman.live:11022/root/goodbuddy-shareserver.git`。
当前服务是交互原型,尚未提供真实 Desktop 集成。共享网络总体设计仍由本文统一维护。
ShareServer 不反向依赖桌面端 `src/shared`,构建和运行不要求桌面仓库存在。
真实跨端协议实现时双方依赖独立 `share-protocol` 包,具体仓库归属和发布方式届时确定,
不提前创建空包。协议包不得包含数据库模型、Electron API、服务实现或凭据。
## 16. 共同验收标准
@@ -442,9 +449,9 @@ src/ # GoodBuddy Desktop
- [设备发现与共享功能逻辑](../features/device-sharing/logic-design.md)
- [设备发现与共享 UI](../features/device-sharing/ui-design.md)
- [设备发现与共享技术设计](../features/device-sharing/technical-design.md)
- [ShareServer PRD](../features/share-server/prd.md)
- [ShareServer 功能逻辑](../features/share-server/logic-design.md)
- [ShareServer 管理界面](../features/share-server/ui-design.md)
- [ShareServer 技术设计](../features/share-server/technical-design.md)
- [ShareServer PRD](../../../goodbuddy-shareserver/docs/features/share-server/prd.md)
- [ShareServer 功能逻辑](../../../goodbuddy-shareserver/docs/features/share-server/logic-design.md)
- [ShareServer 管理界面](../../../goodbuddy-shareserver/docs/features/share-server/ui-design.md)
- [ShareServer 技术设计](../../../goodbuddy-shareserver/docs/features/share-server/technical-design.md)
- [SSH 远程主机与 Agent](../features/remote-host/README.md)
- [GoodBuddy 统一界面设计系统](../../UI-DESIGN.md)
+32 -13
View File
@@ -38,9 +38,19 @@ below.
`npm run release:package -- --platform <platform> --arch <arch>`. It only
packages for the native host and writes to
`dist/release/<platform>-<arch>`.
- Default deliverables are NSIS and portable ZIP for Windows, DMG and ZIP for
- Default deliverables are NSIS and portable ZIP for Windows, DMG for
macOS, and AppImage, DEB, and RPM for Linux. Every target includes
`release-manifest.json` with SHA-256 hashes.
- Starting with Desktop 0.12.8, macOS releases contain DMG only. Older macOS
GitHub update readers and all older mirror readers require ZIP and cannot
validate this new matrix; affected users must upgrade manually from the
download page. The user approved this immediate format change. Current
readers accept both the historical DMG/ZIP matrix and the DMG-only matrix.
- Final Desktop package verification compares every bundled OpenCode
configuration file with the prepared dependency tree and rejects external
DeepSeek imports in the unpacked Host and chunks. Windows x64 CI also
imports the packaged OpenCode plugin and starts an isolated packaged
UtilityProcess before publishing; source-only tests do not replace it.
- `build/build-release.cjs` verifies the unpacked application, `app.asar`,
bundled Continue and OpenCode runtimes, executable architecture, and package
signatures before atomically replacing a release directory.
@@ -50,9 +60,9 @@ below.
optional: users explicitly download a compatible compound `.gbagent` package
from Settings or import one offline before GoodBuddy can install or update a
Host.
- Each independently published Linux x64 or arm64 `.gbagent` contains the
- Each independently published Linux x64, Linux arm64, or macOS arm64 `.gbagent` contains the
Agent daemon, pinned Node, and the desktop-maintained compatible OpenCode
Runtime. Build both architectures on native GitHub Actions runners from one
Runtime. Build all three targets on native GitHub Actions runners from one
immutable `agent-v<agentVersion>` tag.
- Keep the GoodBuddy Agent source, shared protocol/contracts, runtime lock,
bundle tooling, and tests in this repository so a desktop commit identifies
@@ -66,7 +76,9 @@ below.
digest, use only an ephemeral in-memory test signing identity, and verify the
resulting compound package and deterministic archive. It must not read
production signing secrets, publish installable release artifacts, or modify
the checked-in public key registry.
the checked-in public key registry. The Darwin arm64 target uses `macos-15`
and the same production locks. Native CI checks do not replace real Host
installation, Attach, Ask/Execute, and lifecycle validation during development.
- `.github/workflows/agent-release.yml` is the only production compound Agent
publication path. It requires an annotated immutable Agent tag, the protected
`agent-signing` Environment, native x64/arm64 builds, one production
@@ -98,11 +110,18 @@ below.
and are separate from desktop `v${package.version}` releases. Confirm the
exact Agent release commit and tag with the user before creating or pushing
either.
- The tagged commit must be reachable from protected `main`. The two native
jobs build the compound x64/arm64 packages from the same source and locks;
- The tagged commit must be reachable from protected `main`. The three native
jobs build Linux x64, Linux arm64, and Darwin arm64 packages from the same source and locks;
the catalog job rejects missing architectures, changed bytes for an existing
version/architecture identity, invalid prior signatures, and incompatible
version/platform/architecture identity, invalid prior signatures, and incompatible
matrix metadata.
- All three targets share the existing signed `agent-catalog.json`. Before the
first mixed-platform catalog is published, release the Desktop reader that
accepts Darwin entries and document the required Desktop upgrade. Older
Desktop readers reject the entire mixed catalog and cannot check online Agent
updates; preserve their locally verified packages and immutable historical
releases. Do not introduce a second catalog or silently weaken package
signature and integrity verification.
- Agent GitHub Releases are ordinary non-draft, non-prerelease releases but
must use `--latest=false`; they must never replace the desktop release marked
Latest. Publish immutable packages and the versioned catalog to
@@ -202,12 +221,12 @@ surface together before a new release.
It must be able to write immutable version objects and the final latest
pointer without granting unrelated administration privileges.
- Upload release assets and `site-release.json` under the immutable
`releases/<tag>/` prefix first. Verify all 14 installer URLs publicly before
`releases/<tag>/` prefix first. Verify all 12 installer URLs publicly before
creating or publishing the GitHub Release. Update
`releases/latest.json` only after the GitHub Release is public and all prior
checks succeeded.
- The expected GitHub Release contains 22 assets: 14 installers (two formats
for each Windows/macOS target and three formats for each Linux target), six
- The expected GitHub Release contains 20 assets: 12 installers (two formats
for each Windows target, DMG for each macOS target, and three formats for each Linux target), six
renamed target manifests, one aggregate `release-manifest.json`, and one `SHA256SUMS`.
`site-release.json` is an OSS publication artifact, not a GitHub Release
asset.
@@ -260,17 +279,17 @@ Do not report a release complete until all of the following are verified:
2. The public GitHub Release is non-draft, non-prerelease, marked Latest, and
uses the expected tag and title. Its body must exactly match the Markdown
generated from the approved packaged bilingual notes.
3. GitHub release metadata contains exactly the expected 22 uploaded asset
3. GitHub release metadata contains exactly the expected 20 uploaded asset
names, including the expected Windows x64 and arm64 assets. Do not download,
hash, launch, or send individual network probes to published installers
unless the user explicitly requests that additional validation.
4. The Beijing `releases/latest.json` returns HTTP 200, has the expected stable
version, exact six targets and 14 installer entries, the trusted Beijing
version, exact six targets and 12 installer entries, the trusted Beijing
URLs, and the GitHub fallback URL. It must match the immutable
`releases/<tag>/site-release.json`. Fetching and comparing these small JSON
metadata files is sufficient; rely on the successful publication job for
its per-asset public checks.
5. The live website successfully fetches the index and produces the 14 correct
5. The live website successfully fetches the index and produces the 12 correct
platform/architecture/format links from that metadata.
6. Both remote `main` refs and both peeled tag refs still equal the approved
release commit, and the local working tree is clean.
@@ -0,0 +1,34 @@
# 应用导航与系统工具入口
本功能整理 GoodBuddy 主侧栏的入口层级:系统入口保持固定,应用占用主导航,系统工具从
侧栏底部快捷打开助手工作栏。该功能同时为后续插件应用和插件工具预留统一的入口类型,但
不包含插件市场的安装与运行机制。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [PRD](./prd.md) | 用户问题、产品边界、功能要求与产品验收 |
| [User Stories](./user-stories.md) | 用户场景及 Given/When/Then 验收 |
| [功能逻辑设计](./logic-design.md) | 分类规则、入口状态、优先级与失败结果 |
| [UI 设计](./ui-design.md) | 侧栏信息架构、控件、文案、键盘和响应式行为 |
| [技术设计](./technical-design.md) | 现有模块映射、状态契约、持久化和实施顺序 |
## 术语
| 术语 | 定义 |
| --- | --- |
| 系统入口 | GoodBuddy 工作流程或审计所必需的固定入口,用户不能卸载、隐藏或改变类型 |
| 应用 | 在主内容区打开完整一级页面的功能,可显示在“当前对话”所在的主导航区 |
| 系统工具 | GoodBuddy 自带且不可卸载的辅助能力,包括文件、终端、浏览器和资源监控,从侧栏底部打开助手工作栏中的对应面板 |
| 插件应用 | 未来由插件提供的一级页面入口 |
| 插件工具 | 未来由插件提供的辅助面板或命令入口 |
| 快捷入口 | 只负责打开或聚焦能力,不复制该能力的界面和业务状态 |
## 相关文档
- [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md)
- [通用助手工作栏与执行空间 PRD](../assistant-workbar/prd.md)
- [助手工作栏多终端页签 PRD](../assistant-workbar/terminal-tabs-prd.md)
- [智能心跳 PRD](../smart-heartbeat/prd.md)
- [知识库功能文档](../knowledge-base/README.md)
@@ -0,0 +1,167 @@
# 应用导航与系统工具入口功能逻辑设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.2 |
| 日期 | 2026-09-08 |
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
| 关联 User Stories | [User Stories](./user-stories.md) |
## 1. 不变量
1. “当前对话”“运行记录”和“设置”始终是系统入口。
2. 运行记录不进入工具注册表、应用显隐设置或插件市场。
3. 文件浏览器、终端、浏览器和资源监控始终是系统工具,不能卸载或被插件替换。
4. 左下角只提供快捷入口,工具内容仍由助手工作栏承载。
5. 隐藏应用不等于停用功能,也不删除、迁移或清理业务数据。
6. 快捷入口不能扩大执行空间、文件、PTY、网络或浏览器权限。
## 2. 入口模型
```ts
type NavigationContribution =
| {
type: 'system-entry'
id: 'chat' | 'activity' | 'settings'
placement: 'primary-start' | 'primary-end' | 'footer'
}
| {
type: 'application'
id: 'magic-notes' | 'knowledge' | 'heartbeat'
source: 'builtin'
visible: boolean
}
| {
type: 'system-tool'
id: 'workspace' | 'terminal' | 'browser' | 'resources'
workbarAppId: 'workspace' | 'terminal' | 'browser' | 'resources'
}
```
该模型只覆盖首期入口。插件贡献模型随插件 Host、权限和生命周期另行设计。
## 3. 主导航组合规则
主导航按以下顺序计算:
```text
当前对话
可见的内置应用
运行记录
```
当前对话和运行记录不参与排序。首期内置应用默认顺序为魔法笔记、知识库、智能心跳;显隐
设置只从中移除对应项,不改变固定入口的位置。
### 3.1 应用可见性与业务状态
| 导航可见 | 功能启用 | 结果 |
| --- | --- | --- |
| 是 | 是 | 显示入口并允许进入 |
| 否 | 是 | 隐藏入口,后台和数据保持原状 |
| 是 | 否 | 不显示不可用入口;设置页保留启用路径 |
| 否 | 否 | 不显示入口,保留符合该功能自身规则的数据 |
“功能启用”由各应用现有产品设置负责。例如魔法笔记启停仍使用原设置;导航偏好不能覆盖它。
## 4. 系统工具打开规则
### 4.1 通用流程
```text
用户激活工具按钮
-> 校验按钮仍可用
-> 向助手工作栏发送打开意图
-> 工作栏按应用实例策略查找或创建实例
-> 展开工作栏
-> 激活目标实例
-> 把焦点移入面板标题或首个主要控件
```
若创建失败,工作栏保持展开并在对应能力位置显示错误;主页面和其他工作栏实例不被清空。
### 4.2 决策表
| 工具 | 匹配实例存在 | 匹配实例不存在 | 上下文不可用 |
| --- | --- | --- | --- |
| 文件 | 激活单实例并保留其跟随或固定状态 | 创建跟随当前上下文的 `workspace` 单实例 | 打开面板并显示目录修复入口 |
| 终端 | 激活当前执行空间最近的 `starting` 或 `running` 实例 | 创建一个绑定当前执行空间的实例 | 没有项目时使用本机 Home |
| 浏览器 | 激活单实例并保留其目标状态 | 创建跟随当前会话的 `browser` 单实例 | 打开面板并提示选择或创建对话 |
| 资源监控 | 激活 `resources` 单实例 | 创建本机 `resources` 单实例 | 部分指标不可用时显示原因并保留其余指标 |
文件和浏览器按以下优先级处理目标:已有有效固定目标、跟随模式的当前上下文、新实例的默认
上下文。固定目标失效时显示该目标的修复状态,不回退到当前项目、默认工作区或当前会话。
`closing`、`exited`、`interrupted` 和 `failed` 终端不属于可复用实例。当前执行空间只有这些
终端时,快捷按钮创建新实例并保留原 Tab 的历史状态。用户需要额外的并行终端时使用工作栏
“+”或终端面板中的既有创建命令。
### 4.3 选中与状态
工具按钮满足以下条件时显示选中:助手工作栏已展开,且当前活动实例的 `appId` 与工具映射
一致。只有实例存在但未激活时不显示选中。
徽标优先级为:等待用户处理 > 错误 > 运行中 > 无徽标。同一按钮只显示一个视觉徽标,
可访问名称包含完整状态。
资源监控首期不在后台采样,因此不显示负载告警徽标。只有资源面板是工作栏当前活动实例且
工作栏已展开时,资源按钮才显示选中状态。
### 4.4 资源采样状态
```text
inactive -> warming -> loading -> live
-> partial
-> error
live/partial/error -> loading
warming/loading/live/partial/error -> inactive
```
- `inactive`:资源面板未处于可见活动状态,没有采样定时器。
- `warming`:首次进入或停止后恢复,建立 CPU 和磁盘 IO 区间基线;内存仍可立即显示。
- `loading`:正在请求后续样本;请求未结束时跳过后续时钟触发。
- `live`:所有首期指标可用,按 2 秒间隔追加样本。
- `partial`:至少一个指标可用;不可用项携带稳定错误码,其他指标继续更新。
- `error`:本次没有任何可用指标,保留已有历史;自动时钟或手动操作可以重试。
资源面板为活动实例、工作栏展开且 `document.visibilityState` 为 `visible` 时才离开 `inactive`。
任一条件失效即停止定时器并重置区间基线。历史队列只保留最后 150 个样本;时间间隔较长或
采样失败时保留真实时间缺口,不补零或伪造中间数据。
## 5. 设置保存规则
应用导航偏好与应用业务启停分开保存。Switch 每次切换立即提交完整目标集合;请求完成前禁用
三个应用显隐 Switch,下一次操作基于最新确认结果提交,防止并发更新互相覆盖。
保存失败时:
- 主导航和 Switch 恢复最后一次成功保存的偏好。
- 设置行显示可重试错误。
- 不回滚或修改各应用的业务状态。
## 6. 与需求的对应关系
| 逻辑 | PRD | User Stories |
| --- | --- | --- |
| 固定入口不变量 | FR-1、FR-2 | US-A1、US-A2 |
| 应用显隐和失败恢复 | FR-3、FR-4、FR-11、FR-12 | US-B1、US-B2、US-B4、US-B5 |
| 工具打开决策表 | FR-5 至 FR-10 | US-C1 至 US-C7 |
| 资源采样状态 | FR-9 | US-C6 |
| 无障碍与窄窗口 | FR-13 | US-D1、US-D2 |
## 7. 逻辑完整性评估
首期规则已覆盖固定入口、应用显隐、保存失败、系统工具单实例与多实例差异、本机 Home、
托管 SSH、无活动会话、目录失效、资源部分可用、采样停止、权限边界、键盘和窄窗口。其他
工具面板内部的取消、超时、
连接恢复和固定目标失效继续由助手工作栏负责,本功能不复制这些状态机。
## 8. 后续产品决策
- 插件工具上线后,快捷栏允许固定的最大数量及溢出规则。
- 插件应用是否允许用户排序,以及固定系统入口之间允许的排序范围。
这些决策不影响首期四个系统工具的固定入口和内置应用显隐实现。
@@ -0,0 +1,175 @@
# 应用导航与系统工具入口 PRD
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.2 |
| 日期 | 2026-09-08 |
| 适用产品 | GoodBuddy 桌面端 |
| 关联设计 | [统一界面设计系统](../../../UI-DESIGN.md)、[助手工作栏 PRD](../assistant-workbar/prd.md) |
## 1. 背景
主侧栏当前把“当前对话”“魔法笔记”“知识库”“智能心跳”和“运行记录”放在同一导航组,
底部长期显示“GB 本地工作区”和平台架构。底部信息不能反映用户可以执行的操作,文件、终端
和浏览器则需要先找到右侧助手工作栏后才能打开。
后续插件可能提供完整页面,也可能只提供辅助面板或命令。若不先区分系统入口、应用和工具,
插件接入后会同时争用主导航、侧栏底部和助手工作栏,用户也无法判断入口会切换页面还是打开
辅助面板。
## 2. 产品定义
### 2.1 三类入口
| 类型 | 首期成员 | 默认位置 | 用户控制范围 |
| --- | --- | --- | --- |
| 系统入口 | 当前对话、运行记录、设置 | 主导航固定位置或侧栏底部 | 不可隐藏、卸载或改变类型 |
| 应用 | 魔法笔记、知识库、智能心跳 | 主导航应用区 | 可控制是否显示;隐藏不删除数据 |
| 系统工具 | 文件浏览器、终端、浏览器、资源监控 | 侧栏底部快捷工具栏 | 首期固定提供,不可卸载 |
运行记录是系统审计页面,不属于工具。文件浏览器、终端、浏览器和资源监控是系统工具;它们
的快捷入口位于左下角,实际界面继续由右侧助手工作栏承载。
### 2.2 后续插件类型
插件市场开放后,插件可以贡献“插件应用”或“插件工具”。插件不能注册、覆盖或替换系统入口,
也不能冒充系统工具。内置应用继续标记为“内置”,不因采用统一注册协议而显示成待安装商品。
## 3. 目标
- 用户从侧栏底部一次点击即可打开文件、终端、浏览器或资源监控。
- 主导航只承载完整页面,辅助工具不占用一级页面位置。
- 运行记录始终可发现,不受应用显示设置和插件状态影响。
- 内置应用可以从一个设置位置控制主导航可见性,隐藏后仍可恢复。
- 为插件应用和插件工具保留明确的入口契约,不提前建设插件安装能力。
## 4. 非目标
- 首期不提供第三方插件搜索、下载、安装、更新、签名验证或卸载。
- 不把文件树、终端或浏览器重新实现到左侧栏。
- 不改变助手工作栏的面板实例、停靠、执行空间和安全边界。
- 不允许隐藏运行记录、当前对话或设置。
- 不因隐藏应用而删除魔法笔记、知识库或智能心跳数据。
- 不把“显示在侧边栏”解释为停止智能心跳计划或后台任务。
- 不提供任意本机程序启动器。
- 首期资源监控不采集远程主机、GPU、显存或网络吞吐,不提供后台告警和历史持久化。
## 5. 功能要求
### FR-1 入口分类
系统必须为每个可见入口声明稳定类型:`system-entry`、`application` 或 `system-tool`。界面位置、
设置项和点击结果均以该类型为准,不根据当前页面临时改变类型。
### FR-2 固定系统入口
“当前对话”固定在主导航首位;“运行记录”固定在应用区之后;“设置”固定在侧栏底部。
这些入口不出现在应用显隐、工具固定或插件管理设置中。
### FR-3 内置应用区
魔法笔记、知识库和智能心跳显示在当前对话与运行记录之间。应用入口打开对应一级页面,保留
现有徽标、范围和页面状态。用户可以在设置中控制其主导航可见性。
### FR-4 隐藏应用
隐藏应用只移除主导航入口,不删除数据、不修改业务启停状态,也不终止正在进行的操作。显隐
Switch 更新成功后主导航立即生效;更新失败时恢复原状态并允许重试。用户可从设置恢复入口。
### FR-5 系统工具快捷栏
侧栏底部提供文件浏览器、终端、浏览器和资源监控四个图标按钮,并保留独立设置按钮。每个
按钮具有可见工具提示和可访问名称。首期系统工具顺序固定为文件、终端、浏览器、资源监控。
### FR-6 文件浏览器入口
点击文件按钮时展开助手工作栏,并打开或聚焦单实例的“工作区”面板。已有实例保持其“跟随
当前上下文”或“固定到指定对象”状态;创建实例时默认跟随当前项目,没有项目时使用已配置
的默认工作区。跟随上下文或新建实例所需的路径不可用时,面板显示修复入口;已有有效固定
目标不受当前项目和默认工作区状态影响。
### FR-7 终端入口
点击终端按钮时展开助手工作栏。当前执行空间有正在启动或运行中的终端时聚焦最近使用的实例;
没有时创建一个绑定当前执行空间的新终端,已退出、已中断或失败的历史 Tab 保持不变。没有
项目时当前执行空间为本机 Home。创建额外并行终端继续使用工作栏现有的“+”目录入口。
### FR-8 浏览器入口
点击浏览器按钮时展开助手工作栏,并打开或聚焦单实例的受控浏览器面板。已有实例保持其目标
状态;新实例默认跟随当前会话。新建或跟随模式下没有活动会话时,面板显示需要选择或创建
会话的空状态;已有有效固定目标继续显示。入口不打开系统默认浏览器,也不创建旁路 WebView。
### FR-9 资源监控入口
点击资源监控按钮时展开助手工作栏,并打开或聚焦单实例的本机“资源监控”面板。面板显示
整机 CPU 使用率、内存使用量与总量、磁盘读写速率,以及 GoodBuddy 桌面进程的 CPU、内存
和进程数摘要。指标不可用时显示具体项目和原因,不能显示为 `0`。
资源面板处于可见活动状态、工作栏已展开且应用窗口未隐藏或最小化时,每 2 秒采样一次,在
Renderer 内存中最多保留最近 5 分钟。任一条件不满足时停止采样;首期不在后台持续监控,
也不据此产生侧栏告警。首次采样及停止后恢复的首次采样只建立 CPU 和磁盘 IO 速率基线,
不显示伪造的 `0` 值。
### FR-10 快捷入口状态
工具面板已激活时,对应按钮显示选中状态;面板存在但未激活时保持普通状态。运行中、错误或
等待用户处理可以显示徽标,但必须有可访问文字,且不得自动抢占当前面板。
### FR-11 应用导航设置
设置页提供“应用与工具”管理入口。首期“应用”区域列出三个内置应用并提供“显示在侧边栏”
Switch,切换后立即持久化。系统工具区域列出四项内置固定能力,不提供虚假的卸载或停用按钮。
### FR-12 偏好持久化
应用可见性按设备持久化。升级时保留当前行为:已有可用内置应用继续显示;魔法笔记原有功能
启停设置保持原意,不由新的导航可见性覆盖。
### FR-13 窄窗口行为
窄窗口下主侧栏仍按现有抽屉方式显示。系统工具按钮保持单行且不与设置按钮重叠;空间不足时
收起文字和非必要分隔,不隐藏四个系统工具或设置。
## 6. 权限与安全
- 快捷入口不授予新的文件、命令、网络或浏览器权限。
- 系统工具继续使用 Main、Preload、执行空间 Grant、Ask/Execute 和现有审批边界。
- Renderer 不因新增入口获得任意文件系统、PTY、进程或 Electron API。
- 资源指标由 Main 采集并以有界结构返回;Renderer 不接收进程命令行、环境变量或任意 PID。
- 资源样本只保存在当前 Renderer 内存中,不写入运行记录、SQLite 或遥测服务。
- 插件贡献类型只是界面与导航声明,不能代替权限清单、用户授权和运行隔离。
- 运行记录继续记录由系统工具触发的受审计操作。
## 7. 产品验收
- 主导航能明确区分固定系统入口和可配置内置应用。
- 运行记录在所有应用显隐组合下保持可见并可打开。
- 左下角不再用“本地工作区”与系统架构占据主要空间。
- 文件、终端、浏览器和资源监控均可从左下角打开正确的工作栏面板,且不产生第二套内容界面。
- 连续点击系统工具不会无意创建重复的文件或浏览器实例;终端遵守多实例规则。
- 资源监控以 2 秒间隔更新,保留不超过 150 个样本;面板不活动、工作栏收起或窗口隐藏、
最小化后停止请求新样本。
- CPU、内存、磁盘 IO 或 GoodBuddy 指标不可用时,页面区分部分可用与整体失败。
- 隐藏内置应用不删除数据、不停止后台计划,并能从设置恢复;保存失败时入口保持原状。
- 键盘、屏幕阅读器、浅色主题、深色主题和窄窗口均可完成相同操作。
## 8. 分期
### 首期
- 重构侧栏底部,增加四个系统工具按钮和独立设置按钮。
- 建立从 App 到助手工作栏的“打开或聚焦工具”意图接口。
- 增加本机资源快照采集与资源监控单实例面板。
- 增加内置应用的主导航显示设置。
- 保持运行记录为固定系统入口。
### 后续
- 插件应用和插件工具注册。
- 插件工具固定、取消固定和排序。
- 插件市场、权限详情、安装和卸载。
- 用户级快捷键与命令面板接入同一贡献命令。
@@ -0,0 +1,377 @@
# 应用导航与系统工具入口技术设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.2 |
| 日期 | 2026-09-08 |
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
| UI 设计 | [UI 设计](./ui-design.md) |
## 1. 现状
主页面没有 URL Router,由 `App.tsx` 中的 `WorkspaceView` 和 keep-alive 状态切换:
```ts
type WorkspaceView =
| 'chat'
| 'magic-notes'
| 'knowledge'
| 'heartbeat'
| 'activity'
| 'settings'
```
文件、浏览器和终端已经由 `RightAssistantSidebar`、`WorkbarShell` 与 `workbar-contracts.ts`
承载。工作区与浏览器是单实例应用,终端是多实例应用。代码中尚无资源指标采集和资源监控
面板。现有侧栏底部 `user-card` 整行打开设置,并显示“本地工作区”和平台架构。
本次改动增加入口编排、从主侧栏到工作栏的命令通道,以及本机资源监控的 Main/Preload/
Renderer 最小实现。文件、终端和浏览器后端保持不变。
## 2. 模块边界
| 模块 | 责任 |
| --- | --- |
| `App.tsx` | 组合主导航、系统工具栏、设置入口和页面离开检查 |
| `SidebarSystemTools`(新增) | 渲染四个系统工具按钮并发送打开意图 |
| `RightAssistantSidebar.tsx` | 接收打开意图,调用工作栏实例创建或激活逻辑 |
| `WorkbarShell.tsx` | 继续负责实例策略、Tab、目录和焦点行为 |
| `workbar-contracts.ts` | 继续作为工作栏应用 ID 和实例布局的权威契约 |
| `resource-monitor-contracts.ts`(新增) | 定义本机资源快照、可用性和错误码 |
| `resource-monitor-service.ts`(新增) | 在 Main 中采集并规范化本机与 GoodBuddy 指标 |
| `ResourceMonitorPanel.tsx`(新增) | 按需轮询、保留有界历史并渲染资源面板 |
| `application-settings-contracts.ts` | 增加应用导航偏好 schema 和默认值 |
| `application-settings-store.ts` | 迁移并原子保存应用导航偏好 |
| `PlatformFeaturesSettingsSection` 或独立设置 Section | 编辑“显示在侧边栏”偏好 |
不要在 `SidebarSystemTools` 中读取文件、创建 PTY、操作浏览器或采集系统指标;该组件只发送
有界的工具 ID。
## 3. Renderer 契约
### 3.1 系统工具 ID
```ts
export const sidebarSystemToolIds = [
'workspace',
'terminal',
'browser',
'resources'
] as const
export type SidebarSystemToolId = (typeof sidebarSystemToolIds)[number]
```
该类型可以放在 Renderer 内共享模块。首期不暴露 IPC,因为动作只编排已有工作栏 Renderer
状态;实际能力调用继续走现有 Preload API。
### 3.2 打开意图
`App` 向 `RightAssistantSidebar` 传入单调递增的请求或命令对象:
```ts
type WorkbarOpenIntent = {
requestId: number
appId: 'workspace' | 'terminal' | 'browser' | 'resources'
reason: 'sidebar-system-tool'
}
```
`requestId` 使连续点击同一工具仍能被消费。不要仅设置 `assistantSidebarTab`,因为用户可能已
关闭对应实例,且终端需要按执行空间寻找实例。
建议由 `RightAssistantSidebar` 在消费意图后通过现有 `WorkbarShell` 控制接口完成。文件和浏览器
已有实例时只激活实例,保留其“跟随当前上下文”或“固定到指定对象”状态:
```ts
type WorkbarController = {
openOrActivate(appId: WorkbarAppId): Promise<WorkbarTabInstance>
}
```
若当前 `WorkbarShell` 没有命令式入口,先增加窄的回调或受控请求 Props,不引入全局事件总线。
### 3.3 终端匹配
工作栏“+”目录选择终端时每次创建新实例;侧栏系统工具按钮执行“进入当前终端”,采用:
1. 解析当前项目对应的 `ExecutionSpaceSelection`。
2. 在现有终端实例中按目标 identity 查找 `starting` 或 `running` 状态。
3. 存在多个匹配实例时激活最近使用的实例。
4. 没有匹配实例时调用现有终端创建流程。
`closing`、`exited`、`interrupted` 和 `failed` 实例保留为历史 Tab,不阻止快捷按钮创建新终端。
工作栏“+”继续按原规则创建额外终端,不受快捷按钮去重影响。
### 3.4 资源监控工作栏应用
在 `workbar-contracts.ts` 中增加 `resources`:
```ts
{
id: 'resources',
instancePolicy: 'single',
defaultContext: 'application'
}
```
该应用不加入升级后的默认打开实例。侧栏资源按钮与工作栏“+”目录都通过既有单实例规则创建
或聚焦它。
## 4. 应用导航偏好
### 4.1 设置结构
在 `ApplicationSettings` 增加独立字段:
```ts
type BuiltInApplicationId = 'magic-notes' | 'knowledge' | 'heartbeat'
type ApplicationNavigationSettings = {
hiddenBuiltInApplicationIds: BuiltInApplicationId[]
}
```
默认值为空数组,保持升级前所有可用入口可见。ID 数组可以避免为每个应用增加一个顶层布尔
字段;Zod schema 必须去重、限制数量并拒绝未知内置
ID。
首期不保存排序,因为内置应用顺序固定。插件偏好在插件系统确定稳定 identity 和卸载保留
规则后另行扩展,不提前混入当前 schema。
### 4.2 与现有开关组合
入口可见条件为:
```ts
const visible = featureEnabled && !hiddenBuiltInApplicationIds.includes(id)
```
`magicNotesEnabled` 继续控制功能启停。知识库和智能心跳若未来增加业务启停开关,也按同一组合
规则处理。功能停用时设置页保留显隐偏好但禁用显隐 Switch,并提供原功能设置入口。导航设置
不能写回现有开关。
### 4.3 持久化与 IPC
沿用现有应用设置链路:
```text
Renderer
-> window.goodbuddy.updates.updateSettings
-> settings:application:update
-> application-settings-store
-> application-settings.json
```
更新继续由 Main 校验可信 sender、Zod 输入和原子写入。设置版本迁移只补充默认空数组,不
修改已有 `magicNotesEnabled` 或其他平台设置。
## 5. 侧栏底部重构
现有 `user-card` 是一个覆盖整行的设置按钮。实现时替换为容器和相邻独立按钮:
```tsx
<div className="sidebar-footer">
<SidebarSystemTools onOpenTool={openSystemTool} />
<button aria-label={t('navigation.settings')} ...>
<Settings aria-hidden="true" />
</button>
</div>
```
不能在设置按钮内部嵌套工具按钮。设置按钮继续调用 `navigateFromSidebar('settings', trigger)`,
保留设置预加载、焦点恢复和未保存离开确认。
系统工具按钮调用统一 `openSystemTool`:
1. 设置工作栏打开意图。
2. 展开工作栏。
3. 由工作栏创建或激活实例。
4. 按现有策略把焦点移入目标面板。
工作栏仍由窗口右上方的固定开关关闭,关闭后的焦点返回该开关。
## 6. 工作栏状态与快捷按钮
`RightAssistantSidebar` 应向 `App` 暴露当前活动实例的 `appId` 和必要的有界状态摘要,或由
现有 `onTabChange` 扩展得到。主侧栏不读取终端输出、文件内容、URL 或活动详情。
```ts
type SidebarSystemToolState = {
activeToolId?: SidebarSystemToolId
statusByToolId?: Partial<Record<SidebarSystemToolId, 'attention' | 'error' | 'running'>>
}
```
首期可以只实现 `activeToolId`,没有可靠聚合来源时不显示状态徽标。不能根据局部事件猜测错误
或运行状态。
## 7. 应用显隐更新
应用显隐 Switch 复用平台功能设置的即时更新模式。请求开始后禁用三个应用显隐 Switch,只
允许一个完整目标集合处于提交中。Main 返回成功结果后更新 App 中的平台设置快照,主导航
随之变化。失败时恢复控件原值,在对应设置行显示可重试错误;主导航继续读取最后一次成功
保存的设置。
## 8. 插件兼容边界
首期不增加插件注册表、插件 schema 或插件持久化字段,只保留系统 ID 不向插件开放的产品边界。
插件市场设计需要单独确定 Host、权限、签名、生命周期和稳定 identity;不得直接复用 DeepSeek
Harness 的 npm 插件市场。
## 9. 本机资源监控
### 9.1 采集范围
首期在 Main 中使用经过依赖审查的 `systeminformation` 获取整机 CPU、内存和磁盘 IO,使用
Electron `app.getAppMetrics()` 汇总 GoodBuddy 桌面进程 CPU、内存和进程数。采集函数不接收
Renderer 提供的路径、PID、设备名或命令。
首期指标定义:
- CPU:整机忙碌时间占比,规范化为 `0` 至 `100`。
- 内存:总字节数、可用字节数和 `总量 - 可用量` 得到的使用量。
- 磁盘 IO:所有可用本机磁盘的每秒读取与写入字节数。
- GoodBuddy:`app.getAppMetrics()` 返回的桌面进程聚合 CPU、工作集内存和进程数量。各进程
CPU 先相加,再按逻辑处理器数量归一化为整机容量占比 `0` 至 `100`;不能通过截断总和处理
多核数值。
受管 Runtime、MCP 和终端子进程的逐进程指标不在首期范围。磁盘 IO 在平台或权限不足时返回
不可用错误码,不能回退为 `0`。新增依赖在合入前需要完成许可证、维护状态、打包体积和供应链
检查;检查未通过时应选择提供同等跨平台语义的 Main 侧实现,不能改用 Renderer Shell 命令。
### 9.2 快照契约
```ts
type ResourceMetric<T> =
| { status: 'available'; value: T }
| { status: 'warming-up' }
| { status: 'unavailable'; reason: ResourceMetricUnavailableReason }
type LocalResourceSnapshot = {
sampledAt: string
host: {
cpuPercent: ResourceMetric<number>
memory: ResourceMetric<{
usedBytes: number
totalBytes: number
}>
diskIo: ResourceMetric<{
readBytesPerSecond: number
writeBytesPerSecond: number
}>
}
goodbuddy: {
cpuPercent: ResourceMetric<number>
memoryBytes: ResourceMetric<number>
processCount: ResourceMetric<number>
}
}
```
所有数字必须有限且非负;百分比限制为 `0` 至 `100`,字节数设置跨 IPC 上限。`sampledAt` 由
Main 生成。不可用原因使用稳定枚举,例如 `unsupported-platform`、`permission-denied`、
`collector-timeout` 和 `collector-failed`,不向 Renderer 返回原始命令输出。`warming-up` 只用于
依赖两个采样点的 CPU 与磁盘 IO,不能用于掩盖采集失败。
### 9.3 Preload 与 IPC
Preload 只暴露:
```ts
resourceMonitor.getLocalSnapshot(input: {
resetIntervalBaseline: boolean
}): Promise<LocalResourceSnapshot>
```
对应 IPC 为 `resource-monitor:local-snapshot:get`。Main 验证可信 sender;单次采集设置超时,
并合并同一时刻的并发请求,防止多个 Renderer 重复启动系统采集。采集超时后向调用方返回
有界错误,但底层 Promise 仍占用唯一单飞槽位,直到实际结束;在此之前不启动第二个采集
任务。输入只接受布尔值,接口不提供任意进程、磁盘或远程主机查询。
合并规则区分普通请求与基线重置请求:
- 普通请求在途时,新的普通请求复用它。
- 普通请求在途时收到重置请求,Main 最多登记一个待执行重置;当前采集结束后再执行,不能
用普通请求结果满足重置请求。
- 多个重置请求复用同一个在途或待执行重置。
- 重置请求在途时,普通请求复用其 `warming-up` 结果,下一次时钟再取得区间值。
因此服务最多持有一个在途采集和一个待执行重置。调用方超时不取消这个顺序,也不增加队列
长度。
面板每次从非活动状态恢复后的首次请求传入 `resetIntervalBaseline: true`。Main 消费本次
`systeminformation` 与 Electron CPU 读数并重置区间基线,CPU 和磁盘 IO 返回 `warming-up`;
内存等瞬时指标正常返回。后续请求传入 `false` 并计算当前区间值,避免把面板停用期间计入
速率。
### 9.4 轮询与历史
`ResourceMonitorPanel` 仅在工作栏展开、`resources` 为活动实例且
`document.visibilityState === 'visible'` 时启动轮询:
1. 进入活动状态后立即请求一次,并设置 `resetIntervalBaseline: true`。
2. 此后每 2 秒请求一次;前一请求未结束时跳过本次时钟。
3. Tab 失活、工作栏收起、页面变为隐藏或组件卸载时清理定时器,并忽略较晚返回的旧请求。
4. Renderer 环形队列最多保留 150 个带真实时间戳的样本。
5. 采集失败不追加伪造样本,也不持久化历史。
资源数据不写入应用设置、工作栏布局、运行记录或 SQLite。工作栏布局只保存 `resources` 实例
及其活动状态。
## 10. 测试
### 10.1 Renderer 测试
- 固定入口顺序不受三个应用显隐组合影响。
- 运行记录和设置没有显隐控件。
- 四个系统工具按钮具有名称、工具提示、键盘行为和选中状态。
- 文件和浏览器复用单实例;终端优先聚焦当前执行空间实例。
- 工具按钮展开工作栏但不改变当前主页面。
- 显隐更新成功后主导航立即变化,失败时 Switch 和主导航恢复原状态。
- 窄窗口下工具按钮不重叠,焦点恢复正确。
- 资源面板只在可见活动时轮询,跳过重叠请求并把历史限制为 150 项。
- 窗口隐藏、最小化和恢复时停止轮询并重新建立区间基线。
- 资源部分可用、全部失败和重新进入活动状态均显示正确结果。
### 10.2 Main 与契约测试
- 新设置字段默认值、合法 ID、重复 ID、数量上限和未知 ID 校验。
- 旧版本设置迁移后默认显示所有既有应用。
- 更新导航偏好不修改魔法笔记启停等无关字段。
- 原子写入失败时保留最后一次有效设置。
- 资源快照拒绝 NaN、Infinity、负数字节数、越界百分比和未知错误码。
- 采集超时、权限不足和磁盘 IO 不支持时返回有界部分结果。
- 超时采集尚未实际结束时保持单飞,不累积新的后台采集任务。
- 普通采集在途时收到基线重置请求,最多排队一个重置且不复用普通结果。
- `resetIntervalBaseline: true` 时把 CPU 与磁盘 IO 标记为 `warming-up`。
- GoodBuddy CPU 预热时,其内存和进程数仍可独立返回。
- GoodBuddy 多进程 CPU 按逻辑处理器数量归一化,结果保持在 `0` 至 `100`。
- GoodBuddy 摘要不包含 PID、命令行、环境变量或其他进程信息。
### 10.3 回归范围
- 工作栏布局恢复和已关闭实例重建。
- 本地与托管 SSH 终端目标。
- 浏览器无会话、创建中和 Agent 操作中状态。
- 设置页未保存离开确认。
- 魔法笔记待办数和智能心跳建议数徽标。
- 浅色、深色、`220px` 与 `420px` 侧栏宽度。
- Windows、macOS 和 Linux 的真实 CPU、内存与磁盘 IO 采集;不支持项显示准确原因。
## 11. 实施顺序
1. 增加应用导航设置 schema、迁移和设置界面。
2. 按即时保存结果组合主导航,并补充失败恢复。
3. 增加资源快照契约、Main 采集服务和 Preload API。
4. 为工作栏增加 `resources` 单实例和 `ResourceMonitorPanel`。
5. 拆分侧栏底部设置按钮,加入四个系统工具按钮。
6. 为工作栏增加 `openOrActivate` 意图接口和终端匹配规则。
7. 接入选中状态、焦点恢复、无障碍文案和响应式样式。
8. 完成 Renderer、契约、迁移和跨平台回归测试。
插件注册与插件市场不与首期改动一并实施。
@@ -0,0 +1,203 @@
# 应用导航与系统工具入口 UI 设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.2 |
| 日期 | 2026-09-08 |
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
| 全局规范 | [GoodBuddy 统一界面设计系统](../../../UI-DESIGN.md) |
本文定义入口、设置和轻量资源监控界面。文件、终端和浏览器的内容布局继续遵守助手工作栏
文档。
## 1. 侧栏信息架构
```text
品牌
项目选择
新对话
搜索
当前对话
魔法笔记 3
知识库
智能心跳 1
运行记录
最近对话
...
────────────────────────
[文件] [终端] [浏览器] [资源] [设置]
```
“当前对话”和“运行记录”属于固定系统入口。三个内置应用位于二者之间。侧栏底部不再显示
“本地工作区”和 `platform · arch`;排障信息移至关于或诊断页面。
## 2. 底部系统工具栏
底部使用连续平整表面和顶部分隔线,不做成账户卡片或嵌套卡片。按钮顺序固定:
| 位置 | 图标 | 可见工具提示 | 点击结果 |
| --- | --- | --- | --- |
| 1 | `FolderOpen` | 文件 | 打开或聚焦工作区面板 |
| 2 | `SquareTerminal` | 终端 | 打开或聚焦当前执行空间终端 |
| 3 | `Globe` | 浏览器 | 打开或聚焦受控浏览器面板 |
| 4 | `Activity` | 资源监控 | 打开或聚焦本机资源监控面板 |
| 末位 | `Settings` | 设置 | 打开设置页面 |
图标使用项目现有 Lucide 库。每个按钮采用统一的 `34px × 34px` 点击区域,内部图标建议
`16px`。工具栏使用 `8px` 内边距和 `4px` 间隔;设置按钮通过自动空白靠右,不添加第二条
视觉分隔线。
### 2.1 状态
- 默认:透明背景,图标使用次级文字色。
- 悬停:使用弱表面色,不能强于当前选中状态。
- 选中:使用强调选中背景,并提高图标对比度。
- 键盘焦点:显示统一焦点环,不能被侧栏边缘裁切。
- 处理中:创建或激活请求未完成时暂时禁用对应按钮,保留尺寸并提供进行中名称。
- 徽标:只用于等待处理、错误或活动状态,不显示无意义的常驻绿色圆点。
选中状态表达“对应工作栏面板正在显示”,不表达功能已安装或权限已授予。
## 3. 主导航应用区
应用入口沿用现有 `nav-item` 行,不使用应用卡片。图标、名称和徽标保持单行:
```text
[图标] 魔法笔记 3
[图标] 知识库
[图标] 智能心跳 1
```
隐藏某个应用后,其余应用向上补位。运行记录始终位于最后一个可见应用之后,不因应用显隐
改变样式或归类为工具。
设置页打开期间,主侧栏显示最后一次成功保存的导航状态。Switch 更新成功后导航立即变化,
失败时保持原导航。
## 4. 应用与工具设置
设置中心新增“应用与工具”分类,采用 `standard` PageShell:
```text
应用与工具
管理主导航中的应用。系统工具由 GoodBuddy 提供并固定在侧栏底部。
应用
魔法笔记 笔记、待办和评论 [显示在侧边栏 开]
知识库 管理和检索知识 [显示在侧边栏 开]
智能心跳 查看计划、建议和运行历史 [显示在侧边栏 开]
系统工具
文件 打开当前工作区 内置
终端 进入当前执行空间 内置
浏览器 打开 GoodBuddy 受控浏览器 内置
资源监控 查看本机 CPU、内存和磁盘 IO 内置
系统入口
当前对话、运行记录和设置位置固定,不参与上述配置。
```
应用显隐使用共享 Switch,标签必须是“显示在侧边栏”,不能缩写为“启用”。切换时立即保存,
请求期间禁用三个应用显隐 Switch;失败后恢复原值,并在对应设置行显示可重试错误。系统工具
使用紧凑只读列表,不渲染禁用的卸载、停用或固定按钮。
功能已停用时,对应应用行显示“功能已停用”和设置入口,保留显隐偏好但暂时禁用 Switch。
若某应用自身支持业务启停,应在该应用原有设置中保留“启用功能”开关,并用说明区分:
- “显示在侧边栏”只控制入口。
- “启用功能”控制该功能是否运行。
## 5. 助手工作栏反馈
点击系统工具后,右侧工作栏按现有布局回流打开。主内容页面保持不变,侧栏不渲染工具内容。
- 文件:激活“工作区”Tab,焦点进入目录区域或错误修复入口。
- 终端:激活匹配终端,焦点进入终端输入区;新建期间保留按钮宽度和进度状态。
- 浏览器:激活“浏览器”Tab,焦点进入工具栏;没有会话时进入空状态主操作。
- 资源监控:激活“资源监控”Tab,焦点进入指标摘要;首个样本加载后开始绘制历史。
后台事件可以更新徽标,不能自动展开工作栏或切换当前 Tab。由用户点击快捷按钮触发的打开
属于显式操作,可以展开工作栏并切换 Tab。
### 5.1 资源监控面板
资源监控使用紧凑单列,不使用指标卡片墙:
```text
资源监控 本机
CPU 18%
[最近 5 分钟折线图]
内存 9.6 / 32 GB
[使用量进度条]
磁盘 IO 读 12 MB/s · 写 3 MB/s
GoodBuddy
CPU 4% · 内存 620 MB · 桌面进程 7
更新于 14:32:08
```
- CPU 折线纵轴固定为 `0%` 至 `100%`,不能根据当前低负载动态放大波动。
- 内存同时显示使用量、总量和百分比;进度条不代替文字数值。
- 磁盘 IO 使用每秒读写字节并按统一单位格式化,不显示累计设备字节数。
- GoodBuddy 摘要只显示聚合值,不列 PID、命令行或环境变量。
- 首次加载或停止后恢复时,CPU 和磁盘 IO 显示“正在建立采样基线”,不先渲染 `0%`;内存
等瞬时指标可以立即显示。
- 部分可用时只在对应指标行显示“不可用”和简短原因;全部失败时显示重试入口。
- 图表不使用动效平移;`prefers-reduced-motion` 下行为不变。
- 面板失去活动状态、工作栏收起或窗口隐藏、最小化时停止更新;返回后重新建立区间指标基线,
不填补离开期间的历史。
## 6. 文案
固定使用以下名称:
| 对象 | 文案 |
| --- | --- |
| 设置分类 | 应用与工具 |
| 文件系统工具 | 文件 |
| 终端系统工具 | 终端 |
| 浏览器系统工具 | 浏览器 |
| 浏览器说明 | GoodBuddy 受控浏览器 |
| 资源系统工具 | 资源监控 |
| 资源范围 | 本机 |
| 指标缺失 | 不可用 |
| 应用显隐开关 | 显示在侧边栏 |
| 内置来源 | 内置 |
不再使用“本地工作区”作为侧栏底部身份名称。项目和默认工作区的路径继续由项目选择器、页面
范围或工作区面板显示。
## 7. 键盘和无障碍
- 底部按钮均为独立 `button`,不能嵌套在整行设置按钮中。
- Tab 顺序为文件、终端、浏览器、资源监控、设置。
- 图标按钮通过 `aria-label` 提供名称,工具提示不能作为唯一可访问名称。
- 工作栏打开后按现有焦点策略聚焦目标面板;用户通过固定工作栏开关关闭时,焦点返回该开关。
- 选中按钮使用 `aria-pressed` 或等价的可访问状态,不能只依靠背景色。
- 徽标的数量或状态合并进按钮可访问名称,例如“终端,1 个等待处理”。
- 应用显隐 Switch 支持 Space 切换,保存失败后恢复原值并保留焦点。
## 8. 响应式
- 侧栏宽度 `220px` 至 `420px` 时,底部按钮尺寸不随宽度缩放。
- 窄窗口继续使用现有侧栏抽屉;工具栏不转为多行。
- 空间不足时缩小设置按钮前的弹性空白,不隐藏四个系统工具和设置。
- 工具提示不得超出窗口;靠近底边时显示在按钮上方。
- 粗指针设备不依赖悬停,按钮的可访问名称和选中反馈保持有效。
## 9. 未来插件入口
插件市场交付后另行定义插件区域、固定数量、溢出和权限界面。系统入口与四个系统工具的位置
保持保留,不向插件开放。
@@ -0,0 +1,228 @@
# 应用导航与系统工具入口 User Stories
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.2 |
| 日期 | 2026-09-08 |
| 关联 PRD | [应用导航与系统工具入口 PRD](./prd.md) |
| 关联逻辑 | [功能逻辑设计](./logic-design.md) |
## 1. 角色
### 1.1 普通用户
使用对话、笔记和知识功能,希望常用入口稳定,不需要理解工作栏实例模型。
### 1.2 本地工作用户
经常查看项目文件、打开终端、使用受控浏览器或检查电脑负载,希望从任意一级页面快速进入
这些工具。
## 2. Epic A:固定系统入口
### US-A1 始终访问运行记录
作为用户,我希望运行记录保持在固定位置,以便应用入口变化后仍能检查 Agent 和工具活动。
验收:
- Given 用户隐藏任意内置应用
- When 用户查看主导航
- Then “运行记录”仍显示在应用区之后
- And 设置中没有隐藏、卸载或固定运行记录的选项
### US-A2 从固定入口进入设置
作为用户,我希望设置入口始终位于侧栏底部,以便快捷工具状态变化时仍能进入配置。
验收:
- Given 任意主页面和任意侧栏宽度
- When 用户查看侧栏底部
- Then 设置按钮保持可见且点击区域不与工具按钮重叠
- And 点击后继续遵守未保存内容的离开确认
## 3. Epic B:管理应用入口
### US-B1 识别完整应用
作为用户,我希望魔法笔记、知识库和智能心跳显示在主导航,以便知道它们会打开完整页面。
验收:
- Given 对应功能已启用且应用设置为显示
- When 用户打开主侧栏
- Then 应用位于“当前对话”和“运行记录”之间
- And 点击应用在主内容区打开对应一级页面
- And 原有待办或建议徽标继续显示
### US-B2 隐藏低频应用
作为用户,我希望隐藏不常用的内置应用,以便缩短主导航,但保留已有数据。
验收:
- Given 应用已有内容
- When 用户关闭该应用的“显示在侧边栏”Switch
- Then 主导航不再显示该应用
- And 应用数据、计划和业务启停状态不变
- And 用户可从同一设置位置恢复显示
### US-B4 保留功能启停语义
作为魔法笔记用户,我希望导航显隐与功能启停相互独立,以免整理导航时关闭功能。
验收:
- Given 魔法笔记功能已启用
- When 用户只隐藏其侧栏入口
- Then 笔记及待办处理能力继续按原设置工作
- And 再次显示时恢复原页面和数据
### US-B5 恢复显隐保存失败
作为用户,我希望入口设置保存失败时继续使用原导航,以免界面状态与实际设置不一致。
验收:
- Given 应用显隐更新尚未完成
- When 用户切换“显示在侧边栏”Switch
- Then 三个应用显隐 Switch 在请求期间暂时不可操作
- And 更新成功后主导航立即变化
- And 更新失败时 Switch 和主导航恢复原状态并显示可重试错误
## 4. Epic C:使用系统工具
### US-C1 一键查看文件
作为本地工作用户,我希望点击左下角文件按钮后直接查看当前工作区文件。
验收:
- Given 当前项目有有效根目录
- When 用户点击文件按钮
- Then 助手工作栏展开
- And 已有工作区面板时聚焦该实例,没有时创建单实例
- And 文件按钮通过选中状态表明该面板正在显示
### US-C2 处理无效工作区
作为用户,我希望目录不可用时看到修复方法,并收到明确反馈。
验收:
- Given 工作区面板尚未创建或处于跟随模式
- And 当前项目和默认工作区均不可用
- When 用户点击文件按钮
- Then 工作区面板仍打开
- And 面板说明目录不可用并提供选择或修复目录的入口
- And 应用不静默改用其他目录
### US-C3 快速进入终端
作为本地工作用户,我希望点击终端按钮后进入当前执行空间的终端。
验收:
- Given 当前执行空间已有正在启动或运行中的终端
- When 用户点击终端按钮
- Then 工作栏聚焦最近使用的匹配终端,不创建重复实例
- Given 当前执行空间没有终端
- When 用户点击终端按钮
- Then 创建一个绑定该执行空间的新终端
- Given 当前没有项目
- When 用户点击终端按钮
- Then 创建或聚焦使用本机 Home 的终端
- Given 当前执行空间只有已退出、已中断或失败的终端
- When 用户点击终端按钮
- Then 创建新终端并保留原终端的历史状态
### US-C4 使用远程项目终端
作为远程项目用户,我希望同一个终端按钮进入远程项目目录,以免误在本机执行命令。
验收:
- Given 当前项目绑定托管 SSH 执行空间
- When 用户点击终端按钮
- Then 创建或聚焦绑定该远程项目及其权威目录的终端
- And 面板持续显示远程主机和工作目录
- And 连接失败时保留重连与诊断信息
### US-C5 打开受控浏览器
作为用户,我希望浏览器按钮打开 GoodBuddy 受控浏览器,以便查看与 Agent 相同的会话。
验收:
- Given 浏览器面板跟随当前上下文且当前会话已有浏览器会话
- When 用户点击浏览器按钮
- Then 工作栏聚焦同一个浏览器会话
- And 不打开系统默认浏览器或第二个 WebView
- Given 浏览器面板已固定到其他会话
- When 用户点击浏览器按钮
- Then 工作栏聚焦该面板并保持固定目标
- Given 浏览器面板尚未创建或处于跟随模式
- And 当前没有活动对话
- When 用户点击浏览器按钮
- Then 面板说明需要选择或创建对话
### US-C6 查看本机资源
作为本地工作用户,我希望快速查看 CPU、内存和磁盘 IO,以便判断任务变慢是否与本机负载
有关。
验收:
- Given 资源监控面板尚未打开
- When 用户点击左下角资源监控按钮
- Then 助手工作栏创建并激活单实例资源监控面板
- And 页面显示整机 CPU、内存、磁盘读写及 GoodBuddy 桌面进程摘要
- And 页面每 2 秒更新一次,并用图表保留最近 5 分钟
- When 用户切换到其他工作栏 Tab、收起工作栏、隐藏窗口或最小化窗口
- Then 资源监控停止请求新样本
- When 用户再次显示窗口并返回资源监控
- Then 首次采样建立区间指标基线,下一次采样开始显示 CPU 和磁盘 IO 速率
- Given 某项指标在当前平台不可用
- When 页面收到部分结果
- Then 该指标显示“不可用”及原因,其他指标继续更新
### US-C7 从任意应用打开工具
作为用户,我希望在魔法笔记、知识库、智能心跳和运行记录页面也能打开系统工具。
验收:
- Given 用户位于任意一级页面
- When 用户点击文件、终端、浏览器或资源监控
- Then 主页面保持不变
- And 右侧工作栏打开对应面板
- And 工具不会覆盖主内容或伪装成一级页面
## 5. Epic D:可访问性和布局
### US-D1 使用键盘打开系统工具
作为键盘用户,我希望按顺序聚焦并打开左下角工具,以便不依赖鼠标。
验收:
- 每个系统工具和设置都是独立按钮
- Tab 顺序与视觉顺序一致
- Enter 和 Space 均可激活按钮
- 每个纯图标按钮都有可访问名称和工具提示
- 焦点样式不被侧栏边界裁切
### US-D2 在窄窗口使用快捷栏
作为小屏用户,我希望侧栏抽屉中的工具按钮保持可操作。
验收:
- 工具按钮保持单行且不重叠
- 不因宽度不足隐藏文件、终端、浏览器、资源监控或设置
- 触控点击区域符合统一界面规范
- 关闭侧栏抽屉后焦点返回原触发按钮
+3 -1
View File
@@ -8,6 +8,8 @@
| --- | --- |
| [通用助手工作栏与执行空间 PRD](./prd.md) | 工作栏整体产品范围、执行空间和各工作面板归属 |
| [多终端页签 PRD](./terminal-tabs-prd.md) | 多终端 Tab、目标选择、关闭确认和终端生命周期 |
| [实现与验证进度](./progress.md) | 工作区与 Runtime 相关改动的实现事实、验证证据与剩余阻塞 |
| [Runtime 交互边界](./runtime-interactions.md) | 现有原生权限、结构化问答、后台拒绝和非结构化终端交互的适配范围 |
当前尚未拆出独立 User Stories、功能逻辑、UI 设计、技术设计和进度文档;相关内容仍保留在
当前尚未拆出独立 User Stories、功能逻辑、UI 设计和技术设计文档;相关内容仍保留在
两份现有 PRD 中,后续实际维护该功能时按职责拆分。
+45 -14
View File
@@ -36,8 +36,8 @@ Runtime 统一抽象为“执行空间”。
### 2.1 助手工作栏
助手工作栏是 GoodBuddy 中始终可访问的应用级工具容器。它提供稳定能力目录,用户从中
查看任务中心,并按需打开一个或多个监督、Runtime、终端、进程、工作区、浏览器和成果
面板实例。稳定的是能力的可发现性,不是八个同时占据界面的固定面板。
查看任务中心,并按需打开一个或多个监督、Runtime、终端、进程、工作区、浏览器、资源监控
和成果面板实例。稳定的是能力的可发现性,不是九个同时占据界面的固定面板。
工作栏不是:
@@ -83,6 +83,7 @@ Runtime
进程
工作区
浏览器
资源监控
成果
```
@@ -168,7 +169,7 @@ Runtime
- 建立统一范围、执行空间、生命周期、成果和控制契约。
- 复用现有 Project、Conversation、Task、Artifact、Activity 和 Approval 数据。
- 保持 Renderer 无任意文件、进程、PTY、SSH 或 Electron API 能力。
- 保持 Ask 只读、Execute 审批、取消、超时、输出边界和活动审计。
- 保持 Ask 只读、Execute 账号完整授权、取消、超时、输出边界和活动审计。
- 为本机与远程能力提供一致 UI,同时准确表达能力差异。
## 5. 非目标
@@ -202,15 +203,16 @@ Runtime
### 6.2 能力目录与面板实例
能力目录不等于同时打开八个面板。当前产品采用“已打开实例 Tab + 持久新增按钮”的工作栏
能力目录不等于同时打开九个面板。当前产品采用“已打开实例 Tab + 持久新增按钮”的工作栏
模型:Tab 行只显示已打开实例,行末“+”始终可见;点击后在工作栏内容区打开稳定能力目录,
用户再创建或激活应用实例。详细交互以
[助手工作栏多终端页签 PRD](./terminal-tabs-prd.md) 为准。
- 每项始终显示稳定图标,并提供可见标签或可持续查看的工具提示。
- 已打开实例使用 `tablist`、`tab`、`tabpanel`;“+”是位于 `tablist` 外的普通命令按钮。
- 应用注册表声明 `single` 或 `multiple`。重复选择单实例应用时聚焦已有实例;选择多实例
应用时每次创建新实例。
- 应用注册表声明 `single` 或 `multiple`。在工作栏“+”目录中重复选择单实例应用时聚焦已有
实例;选择多实例应用时每次创建新实例。主侧栏系统工具快捷入口可以聚焦当前执行空间中
最近使用的终端,额外终端仍从“+”目录创建。
- 支持方向键、Home、End、Enter、Space、关闭面板和正确焦点恢复。
- 徽标显示未解决数量、等待审批或失败状态,并同时提供文字或可访问名称。
- 用户调整目录顺序、打开实例、停靠位置和尺寸后持久化;关闭实例后能力仍可从目录重新打开。
@@ -366,6 +368,22 @@ Renderer 不接收任意系统 PID 控制能力。控制动作引用 Main 签发
- 显式打开、下载副本或在终端中打开。
- HTML 文件的源码与安全预览。
当前项目目录的文件浏览与 Git 交互遵循以下规则:
- 页签下不重复显示“项目工作区”标题,保留刷新按钮。
- 未提交更改以彩色 Git 字母标识状态:M 修改、A 新增、D 删除、R 重命名、C 复制、
U 未跟踪或冲突、T 类型变化;冲突用危险色区别于未跟踪文件,同时保留原始状态。
- 点击更改项进入该文件 Diff,分别显示已暂存和未暂存差异,增删行使用对应状态色;
删除文件也可查看。目录树中的文件仍打开内容预览。不再提供底部全仓库 Diff 入口。
- 手动刷新或任务结束更新 Git 状态时,同步重读根目录和已展开目录,保留目录展开状态,
清除未展开目录的旧缓存;正在查看的单文件 Diff 同时重读,旧请求不得覆盖新结果。
无首次提交的仓库也应显示新增和暂存文件。
- 未提交更改首批显示 50 项,可继续分批加载其余变更,不能使后续文件尤其是已删除
文件失去 Diff 入口。
- 系统默认应用和资源管理器打开按钮仅用于本机项目,SSH 项目不显示这两个按钮。
目录切换、下载副本、终端打开及 HTML 安全预览仍属于后续能力,不能据此视为已实现。
本机和远程访问都必须由 Main 或远程 GoodBuddy Agent 在对应文件系统上执行路径规范化、相对路径和
符号链接边界检查。Renderer 只能提交受约束的相对路径和已授权范围 ID。
@@ -418,7 +436,16 @@ type BrowserToolbarState = {
浏览器面板只管理 GoodBuddy 受控浏览器,不表示可以控制用户已安装的浏览器。
### 7.8 成果
### 7.8 资源监控
资源监控是本机单实例面板,显示整机 CPU、内存、磁盘 IO 和 GoodBuddy 桌面进程摘要。面板
只在自身可见并处于活动状态时采样,数据保留在 Renderer 有界内存中,不进入运行记录或长期
存储。首期不提供远程主机、GPU、显存、网络吞吐、后台告警或任意进程查看。
指标缺失时保留面板入口并显示不可用原因。资源监控入口、采样频率、数据契约与验收由
[应用导航与系统工具入口](../application-tool-navigation/README.md)定义。
### 7.9 成果
成果面板统一显示全局、项目、Conversation、Task 执行和监督显式产生的独立成果:
@@ -446,7 +473,7 @@ Task 呈现来源,不把 Run 作为导航对象。
- 不使用 `dangerouslySetInnerHTML`,不启用 Electron `webviewTag`。
- 外部打开是明确的用户操作,并说明外部浏览器可能执行脚本或联网。
### 7.9 Composer 与任务上下文
### 7.10 Composer 与任务上下文
工作栏不提供独立“上下文”能力。用户准备下一次模型请求时:
@@ -470,6 +497,7 @@ type WorkbarCapabilityId =
| 'processes'
| 'workspace'
| 'browser'
| 'resources'
| 'results'
type WorkbarTargetRef =
@@ -751,6 +779,7 @@ remote_operations
其中:
- 工作栏偏好只保存能力目录顺序、面板实例、停靠布局、尺寸和目标引用。
- 资源监控只保存面板实例,不保存采样结果或历史曲线。
- 主机表只保存非敏感元数据和加密凭据引用。
- 应用重启后先通过远程 Daemon 的 journal、lease、boot identity 和 event cursor 核对活动
终端、进程与 Runtime;无法确认时标记 `interrupted` 或 `outcome_unknown`。
@@ -770,7 +799,7 @@ Renderer 只通过显式方法访问:
- 终端创建、输入、调整大小、关闭和有界输出订阅。
- 受管进程列表、日志和允许的控制动作。
- Runtime Inspector 快照、事件和允许的控制动作。
- 工作区、成果、浏览器和监督的既有或扩展服务。
- 工作区、成果、浏览器和监督的既有或扩展服务,以及有界的本机资源快照。
每个 Main Handler 都必须验证可信发送者、Zod 输入、对象归属和当前状态。不得暴露 raw
Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
@@ -779,7 +808,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
1. 工作栏能力目录项和面板实例不授予任何能力;权限只由 Main 中的范围和控制契约产生。
2. Ask 在本机和远程 Runtime 边界保持只读。
3. Execute 继续经过现有 Runtime 和审批控制,工作栏不能直接放宽。
3. Execute 使用当前执行账号的完整权限,不另加目录或逐工具审批;工作栏不能绕过 Main、Preload 与用户数据边界。
4. 用户终端和 Agent 工具执行使用不同身份和事件来源。
5. 进程面板只控制 GoodBuddy 受管对象,不接受任意 PID。
6. 本机和远程路径分别在对应文件系统上 canonicalize 并验证符号链接边界。
@@ -850,6 +879,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
### 阶段 1:成果与 Runtime 可观测性
- 本机 CPU、内存、磁盘 IO 和 GoodBuddy 桌面进程摘要的按需资源监控。
- 通用 Artifact Event。
- HTML 工作区和成果的静态即时预览。
- Runtime Inspector Snapshot 与事件。
@@ -881,8 +911,9 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
- 签名、side-by-side GoodBuddy Agent 安装和按需 detached Daemon。
- SSH connection pool、固定 attach relay、Agent 控制协议、Agent 本地 ACP 日志和断线重连同步。
- 远程工作区、Git、终端和受管进程。
- 先完成签名 OpenCode bundle、Ask bubblewrap、Execute 直接进程、官方 ACP channel、
Main 模型网关、取消、超时和活动记录。
- 当前签名 OpenCode bundle 的 Ask/Execute 均直接启动,Ask 在工具边界保持只读,
Execute 使用账号完整权限;官方 ACP channel、Agent 本地模型网关、取消、超时和活动记录
以[远程主机技术设计](../remote-host/technical-design.md)为准。
- OpenCode 闭环稳定后,Continue 与 DeepSeek Harness 复用相同面板和通道契约逐个接入。
- 新增或重新验证 Host 时只保存并探测 Agent/Runtime,不自动安装。用户在 Host 卡片用
单一主按钮启动,并用次级控件选择自动、Host 下载或 GoodBuddy 传输;Host 保存与环境
@@ -910,7 +941,7 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
### 17.1 稳定能力目录与用户控制
- [ ] 八个标准能力在所有主要页面的目录中始终可发现,但不会默认同时打开。
- [ ] 九个标准能力在所有主要页面的目录中始终可发现,但不会默认同时打开。
- [ ] Task Center 继续作为 Task 的单例索引,不删除入口、不复制会话,也不混入 Job、Run 或心跳事项。
- [ ] 项目、会话、Runtime 或主机变化不会无提示地增删能力目录项。
- [ ] 用户可以按需打开、关闭、排序和停靠面板实例。
@@ -986,6 +1017,6 @@ Electron、ChildProcess、PTY、SSH Client、Socket 或文件句柄。
定义 Host 级组件获取、首次准备、更新、回退和项目只验证边界。
- [统一界面设计系统](../../../UI-DESIGN.md) 定义视觉、语义、响应式和无障碍规则。
若其他文档把工作栏描述为八个同时固定显示的栏目、根据项目或 Runtime 自动裁剪的动态入口,
若其他文档把工作栏描述为九个同时固定显示的栏目、根据项目或 Runtime 自动裁剪的动态入口,
或仅属于当前聊天的附属区域,以本文“能力目录稳定、面板实例由用户打开、当前上下文只提供
默认值”的产品决策为准。
@@ -0,0 +1,92 @@
# 工作栏实现与验证进度
## 2026-09-08 Desktop 0.12.8 安装包修复
- 真实 `0.12.7` 安装目录确认 OpenCode 配置缺少整个 `node_modules`。新增测试调用实际
electron-builder 资源复制器,复现相同 ENOENT;把复制源上移到 `.runtime-resources`
后,插件及传递依赖逐文件复制验证通过。
- 真实安装的 DSH bootstrap 存在 `dsh-session-projection` 外部导入,而 Host 解包目录没有
对应依赖。将其加入 Vite bundling,配置回归从失败转为通过;最终包会拒绝同类外部导入。
- Windows x64 CI 已接入包内 OpenCode 插件导入、真实 DSH UtilityProcess 握手及 npm
探针,位于解包目录清理和发布之前。用户要求不做本地打包;CI 探针结果尚待本候选执行,
不能把当前源码回归通过当作真实发布包已通过。
- 本次不改变 Agent `0.11.21` 源码或包。macOS 改为 DMG-only,用户明确接受旧版客户端
手动升级影响;格式与发布规则见[发布手册](../../development/release-runbook.md)。
## 2026-09-08 Execute 目录权限等待修复
- 本机 OpenCode 的内部配置显式允许默认工具权限;每次 Ask 仍设置 deny-all 会话规则并
禁用工具,当前请求精确分配的只读能力例外。Execute 不把 Workspace 当作目录边界。
- `permission.asked` 不再忽略所有非父会话事件。Main 用 OpenCode Session 的 `parentID`
确认属于当前请求的子孙会话后按当前模式回复;其他并行会话不受影响,同一请求只回复
一次。子会话权限不伪装为父会话中的待完成工具。
- 真实 OpenCode `1.18.9` 和候选 `1.18.29` 均已通过本地确定性模型驱动的原生 Task
工作区外读取回归;同一 Runtime 的 Ask 仍不暴露文件与命令工具。
- Continue `1.5.47` 的真实 CLI 已通过 Execute Shell 写入工作区外测试文件;DSH 固定
Host 已通过 Execute 工作区外写入、随后 Ask 拒绝同一写工具,未增加权限策略或人工审批。
- 真实 OpenCode `1.18.29` 已完成原生 Task 后的 yes/no 与自由文本问答回传;父、子会话
提问及后台任务不能交互时的失败路径均有回归测试。Continue 真实 `AskQuestion` 暴露了
QuizService 扁平数据结构与原适配器不一致的问题,现已按锁定版本修正;真实自由文本
回传、yes/no、选项及跳过均验证通过,自定义答案不再误标为选项。
- DSH 活跃 Execute 的 ACP 权限回调不再等待第二次 authorizer,Ask 继续拒绝;未添加
通用业务问答工具或从终端日志猜测确认意图。完整范围见
[Runtime 交互边界](./runtime-interactions.md)。
- 当前源码 Linux x64 Agent 通过签名组包、生产安装、真实 Ask/Execute、原生子代理
工作区外写入、重连同一 daemon 和 stop/bootstrap。实机回归同时定位并修复了握手
Base64URL 响应误用 ID 校验及断流未结算等待;13 项 Unix 原生端点测试和连续 20 次
握手通过。两轮真实模型调用合计 10 次,均 completed;本轮测试 Agent 已停止。
- 最终完整回归使用 `npm test -- --reporter=json --outputFile=<临时报告路径>`,并通过
`GOODBUDDY_TEST_OPENCODE_BINARY` 指向已验证版本的候选 OpenCode。结果为 338 个文件、
3597 项通过、0 项失败、61 项按平台/手动条件跳过,报告 `success=true`、退出码 0;
Windows 跳过的 Unix 端点场景另在 Linux Host 运行了上述 13 项原生测试。
- 最终 `npm run typecheck`、`npm run lint`、`npm run release:notes:verify` 和差异空白检查
通过,相关 Markdown 相对链接有效。此前失败、暂停及控制台截断的回归记录由本次完整
JSON 报告覆盖,不代表仍有测试失败。未执行系统休眠/唤醒或本地桌面生产构建、打包;
后续发布仍需对应候选的 CI 构建和跨平台验证。
## 2026-09-08 OpenCode 复用与原生控件恢复
- `OpenCodeRuntime` 复用缓存的 embedded server 前执行有超时的健康检查;健康则保留
client,检查失败、抛错或超时则清除 client、会话映射、会话初始化及待答问题,关闭旧
server 后重建。并发调用共享检查与重建,调用方取消只结束自身等待。
- 请求级 MCP 注册与断开通过实例内 `mutateMcp` 队列串行执行。仅实际发出 `mcp.add`
的名称进入清理列表;注册尚在排队时取消不追加断开。断开的 1 秒超时从出队后开始。
- `App` 按本机/远程、连接方式、项目和 Runtime 选择组成 scope 缓存原生清单,用请求
代次阻止旧 scope 结果回写;同 scope 会话刷新不清空重取,切换会话重置消息级选择,
Runtime 切换状态也按 generation 隔离。清单请求失败或返回 `unavailable`/`partial`
时保留已有可用缓存并有界重试;没有可用缓存时保留新返回的部分数据,因此
`unavailable` 重试为可用 `partial` 后可显示 Agent 和 Command。
验证记录:首轮定向测试 273 项通过、5 项跳过;补充修复后的 Runtime 与生命周期测试
77 项通过,界面定向测试 6 项通过,类型检查通过。修复前的 lint 检查通过。
全量测试超过 120 秒被终止,期间出现一项 DS 本地 stdio MCP 测试失败,尚待定位。
这是当次历史结果。后续发布前修复与实机验证见本文上方及
[远程主机技术设计](../remote-host/technical-design.md);真实系统休眠恢复仍未完成验证。
## 2026-09-06 项目工作区
范围见 [PRD 工作区](./prd.md#76-工作区)。
当前源码已移除重复标题与底部完整 Diff 入口,使用彩色 Git 状态字母,点击更改项读取
单文件 Diff。已暂存与未暂存差异分开展示;本机 Git 测试覆盖无首次提交、未跟踪、
暂存后继续修改、重命名、删除及包含方括号的路径。Git 刷新驱动目录树重新读取,保留
展开状态;SSH 项目不提供本机系统打开按钮。审查后补齐了当前 Diff 随刷新重读、
旧请求结果失效,以及超过 50 项变更的继续加载入口。
验证记录:
- 最终 `npm run typecheck`、`npm run lint`、`npm run build` 均通过。
- 工作区专项 7 个测试文件复跑:317 项通过,3 项 Linux 专用测试在 Windows 跳过。
任务完成测试同时检查目录树出现新文件。
- 最终 `npm test`:326 个测试文件、3552 项测试通过,9 个文件、58 项测试按既有条件
跳过。复核首轮曾有一项浏览器地址草稿测试超时,单独复跑及最终全量复跑均通过;
更早的超时/失败记录不再作为最终验收结果。
- 后续复核已通过既有凭据和固定 Host Key 经 VPN 连接共享 Linux x64 Host,使用当前
源码构建的隔离 Agent 包安装并启动。真实 `RemoteWorkspaceAccess → Agent git/status`
与 `git/diff` 路径覆盖暂存后继续修改、删除、重命名、未跟踪和方括号路径;均通过。
Git 验证未调用模型。
- Windows 隔离桌面实例已通过真实点击验收:当前项目单文件暂存/工作树 Diff 分组与
增删颜色正确,修改测试文件后点击刷新可更新正在查看的 Diff,53 项变更经加载更多
全部可访问。截图留在本地验证目录,不纳入产品包。
目录分页、任意目录切换和 HTML 预览不在本轮改动范围内。
@@ -0,0 +1,25 @@
# Runtime 交互边界
本文定义当前原生交互的适配范围,不增加工作模式或第二层 Execute 审批。
| Runtime | 权限确认 | 业务问答 |
| --- | --- | --- |
| 本机 GoodBuddy 管理的 OpenCode | 内部默认配置允许权限;每个 Ask 请求仍使用 deny-all 与显式只读能力。Execute 自动回复属于当前请求的父、子孙会话权限事件,不处理其他并行请求 | 通过 `question.asked` 转交父、子孙会话问题,支持原生单选、多选、yes/no、自由文本及跳过;使用公开 ID 回传 `question.reply/reject` |
| Continue | Execute 启动传入 `--auto`,剩余原生权限请求按当前模式自动答复 | 对接 `1.5.47` QuizService 的扁平 `pendingQuestion`,其中 `question` 是字符串。选项回答与自定义文本分别回传正确的 `isCustomAnswer`;跳过发送拒绝回答的文本 |
| DeepSeek Harness | 固定 Host 的 Execute 文件与命令工具不询问目录权限;若收到 ACP 权限请求,活跃 Execute 直接选一次性允许,缺少该选项时使用 Runtime 提供的允许选项,Ask 拒绝,不等待额外 authorizer | 固定 Host 当前未提供通用原生业务问答服务,不把普通模型正文误识别成待答协议。模型以普通回复提问时,用户通过下一条消息回答 |
托管 SSH OpenCode 的 ACP 权限处理与直接启动配置由
[远程主机技术设计](../remote-host/technical-design.md)定义;本机 SDK 问答适配不代表远程
ACP 已提供相同业务问答接口。
## 运行与失败
- 结构化 yes/no 是普通选项,不从日志、自然语言或 Shell 输出猜测用户意图。
- 前台问题停留在现有问答卡片,保留回答、提交失败提示与跳过入口。
- 后台任务收到结构化提问时,通过现有 Main 路径明确失败并提示改为前台对话,不永久等待。
- 请求结束或取消后清除对应待答映射;过期回答明确报错,不转交给后来的请求。
- 任意终端程序的密码、OAuth 登录、许可证确认或 stdin 提示不自动回复 `yes`。这类程序应
使用其非交互参数,或由用户在终端完成;不能把权限确认和业务决策混为一谈。Shell
工具保留其现有取消与超时,不新增日志解析或通用交互代理。
实现与真实 Runtime 验证证据见[进度记录](./progress.md)。
+2 -2
View File
@@ -11,8 +11,8 @@
## 1. 背景
智能心跳可以生成摘要、后续任务和记忆候选,但这还不是完整学习。其长期“未来分区记忆”
方向尚未设计,也不承担持续学习、模式挖掘或自动改进:
智能心跳可以生成摘要、后续任务和记忆候选,但这还不是完整学习。它与分区记忆的协同尚未
实施,也不承担持续学习、模式挖掘或自动改进:
- 候选是否改善未来行为没有评估。
- 一条反思是否会被检索和使用并不确定。
@@ -7,7 +7,7 @@
| 文档状态 | 实现与发布验收基线 |
| 设计目标 | 将 DeepSeek Harness 作为 GoodBuddy 的第三个 Agent Runtime |
| Runtime 标识 | `deepseek-harness` |
| 当前依赖基线 | 实际使用的 `@deepseek-ai/dsh-*` 底层库,精确锁定 `0.1.0-rc.8` |
| 当前依赖基线 | 实际使用的 `@deepseek-ai/dsh-*` 底层库,精确锁定 `0.1.2-rc.1` |
| 上游状态 | Developer Preview,允许出现破坏性变更 |
| 上游许可证 | MIT |
| GoodBuddy 目标平台 | Windows、macOS、Linux,x64 与 arm64 |
@@ -139,6 +139,11 @@ Harness 子进程内控制面不能取代 Main 控制面,Main 控制面也不
- GoodBuddy 在 Runtime 重启后可以用现有的有界历史创建新 Harness Session。
Harness Session 只在当前 Runtime 进程生命周期内存在。释放 GoodBuddy 会话时必须同步释放对应 Harness Agent。
固定 Host 显式加载 `dsh-session-projection` 的内存 SessionProjectionRegistry,这是
`0.1.2-rc.1` AgentLoop 的必需依赖;不加载 Session persistence,也不新增磁盘会话副本。
该依赖与其余 DSH 依赖一起由 Vite 打入 Host,不能从 `app.asar.unpacked` 外部导入
仅存在于 `app.asar` 的 npm 包。最终打包检查及 Windows CI 的真实 UtilityProcess
启动探针见 [Runtime 资源与打包](../../../BUILD.md#runtime-资源)。
## 6. 总体架构
@@ -290,6 +295,10 @@ GoodBuddy 的工作模式属于每个请求,不属于 Runtime 进程全局状
5. 缺少准备状态、重复使用、请求标识不匹配时,Control Plane 直接拒绝请求。
6. 同一 Session 只允许一个 Prompt 在途。
若 Runtime 回传 ACP 权限请求,Main 只按活跃 Session 的当前模式答复:Execute 优先选择
`allow_once`,缺少时使用提供的 `allow_always`,不调用第二次人工 authorizer;
Ask、已关闭或未知 Session 拒绝。这里不写入永久授权规则,也不把业务问答当作权限确认。
### 8.5 事件模型
Harness Control Plane 只发送 GoodBuddy 能稳定解释的字段:
@@ -503,7 +512,7 @@ Runtime: GoodBuddy 内置 DeepSeek Harness
模型配置: 跟随 GoodBuddy · 企业网关(qwen-plus)
状态: 已就绪
路径: <受控 Host 路径>
版本: 0.1.0-rc.8
版本: 0.1.2-rc.1
执行权限: 当前用户权限
Host 始终由当前 GoodBuddy 版本提供,不存在自定义 Host 入口。
@@ -78,15 +78,14 @@ validating
| 输入 | 结果 |
| --- | --- |
| 未提供 | 当前工作区根目录 |
| `.` 或合法相对子目录 | 解析到工作区内目录 |
| 绝对路径 | 拒绝 |
| `..` 越界、符号链接越界 | 拒绝 |
| `.` 或相对路径 | 以当前工作区为基准解析,可指向工作区外目录 |
| 绝对路径 | 使用指定目录 |
| `..` 或指向工作区外的符号链接 | 解析到实际目标目录,按当前用户权限执行 |
| 不存在或不是目录 | 拒绝 |
| 当前工作区身份在验证期间变化 | 拒绝本次调用 |
首版进程工具比 Execute 的 OS 权限范围更窄地固定工作目录输入,只是为了保持请求与项目
一致;命令本身仍以当前用户权限运行,可以显式访问该账号有权访问的其他绝对路径。该规则
不是 Runtime OS 沙箱,也不能宣称文件系统 containment。
工作区提供默认目录和相对路径基准,不是 Execute 命令的文件系统边界。单次命令选择其他
工作目录不会修改父请求绑定的项目或工作区,也不会影响后续未指定 `cwd` 的命令。
## 6. Shell 选择
+8 -5
View File
@@ -17,8 +17,9 @@ GoodBuddy 直连模型已经可以读取、列出和写入工作区文本,使
分配的 MCP,但没有内置命令执行工具。模型可以生成代码,却不能运行构建、测试、格式化、
Git 或项目脚本,因此无法独立验证结果。
GoodBuddy 也已经有面向用户专家协作的只读 Subagent,但直连模型不能在自己的工具循环中
主动委派一个聚焦的编程子任务。OpenCode、Continue 和 DeepSeek Harness 一般已经提供
本功能设计时,GoodBuddy 已有面向用户专家协作的只读 Subagent,但直连模型尚不能在工具
循环中主动委派编程子任务。当前专家协作已继承父请求 Ask/Execute 模式并使用本机直连模型
工具,与本功能的 `subagent_delegate` 仍为不同入口。OpenCode、Continue 和 DeepSeek Harness 一般已经提供
Shell、Agent 或 Task 能力,再向它们注入 GoodBuddy 同名工具只会造成重复入口和语义冲突。
本功能只补齐直连模型缺少的基础 Agent 能力:
@@ -65,7 +66,8 @@ Subagent 使用父请求的项目、执行空间、工作模式、直连模型
- Ask 父请求只能创建 Ask Subagent。
- Execute 父请求创建 Execute Subagent。
- 子级不能切换项目、目录、Runtime、模型连接或工作模式。
- 子级不能切换父请求绑定的项目、工作区、Runtime、模型连接或工作模式;Execute 命令的
单次工作目录选择遵循 FR-3。
- 子级不再看到 `subagent_delegate`,首版最大委派深度固定为 1。
- 子任务不是新的顶层 Task 或 Conversation,结果先返回父模型。
@@ -79,7 +81,7 @@ Ask 禁止命令和写入。Execute 是用户对当前执行空间账号的完
### 4.1 包含
- 本机 Windows、macOS、Linux 的前台 Shell 命令执行。
- 当前工作区默认工作目录及合法子目录选择。
- 当前工作区默认工作目录,以及当前用户可访问的本机工作目录选择。
- Exit Code、标准输出、标准错误、Shell、目录、耗时和截断状态。
- 命令超时、用户取消、进程树停止和应用退出清理。
- 新进程使用当前本机工具环境提供的 Node、Python 和 PATH。
@@ -118,7 +120,8 @@ Ask 禁止命令和写入。Execute 是用户对当前执行空间账号的完
### FR-3 工作目录和执行位置
- 默认目录是父请求当前工作区。
- 可选工作目录必须解析为当前工作区本身或其子目录。
- 可选工作目录支持绝对路径、相对工作区路径及指向工作区外目录的路径或符号链接,
不额外限制在工作区内;目标必须是实际存在且当前用户可访问的目录。
- 本机执行只能发生在当前桌面执行空间。
- 当前请求绑定 SSH 执行空间且没有远端进程后端时不注册工具,绝不在桌面目录回退执行。
+23 -1
View File
@@ -24,7 +24,9 @@
- [x] `ModelAgentRuntime` 已有多轮工具循环、取消、上下文压缩和有界结果。
- [x] DeepSeek Harness 已按 Windows PowerShell、macOS/Linux Bash 注册原生 Shell。
- [x] OpenCode 和 Continue 使用各自原生执行工具。
- [x] 当前专家 Subagent 是只读协作流程,不是直连模型可调用的编程工具。
- [x] 专家协作与直连模型可调用的编程工具是不同入口;当前专家继承父请求 Ask/Execute,
使用已启用本机直连模型工具并记录工具活动。模式与 authorizer 传递已由
`subagent-service.test.ts` 覆盖,不把该单元测试作为额外的真实模型或远程执行验收。
- [x] Task/Job 领域文档已经明确 Subagent 不是新的顶层 Task 或 Conversation。
- [x] 当前跨平台进程树清理已经覆盖 Windows `taskkill /T /F` 和 POSIX 进程组。
@@ -84,6 +86,26 @@
GoodBuddy 托管 Node/Python 已可用。
- 远端直连模型进程执行不在首版,后续需要独立增加 Agent 进程协议和真机验证。
## 2026-09-06 命令工作目录限制修复
- FR-3 / US-A3:移除命令工作目录的工作区范围限制。绝对路径、`..` 和指向工作区外
的符号链接均按本机文件系统解析,工作区只作为默认目录和相对路径基准。
- 保留目录存在性、本机执行空间绑定、Ask 拒绝命令、取消、超时和进程树回收。
本次没有改变工作区文件读写工具或远端 Agent 路径。
- `direct-model-process-service.test.ts` 覆盖绝对路径、相对路径、外部目录、符号链接、
不存在的目录和普通文件;当前 Windows 环境通过。
- `model-runtime.test.ts` 新增两个生产工具链回归:模型协议响应提供绝对或相对外部
`cwd`,经 `ModelAgentRuntime → ModelToolProvider → LocalDirectModelProcessService`
启动真实 PowerShell,在专用工作区外测试目录复制文件并读回验证。模型响应为本机
测试替身,没有外部模型调用;不将此记录为真实模型或 macOS/Linux 真机验收。
- 最终聚焦验证:`opencode-runtime`、`opencode-runtime-lifecycle`、
`direct-model-process-service`、`model-tool-provider`、`model-runtime` 五个测试文件
`182 passed, 1 skipped`;`npm run typecheck` 和 `npm run lint` 通过。
- 本次共享工作区的全量 `npm test` 为 `3542 passed, 57 skipped, 1 failed`。唯一失败是
`App.test.tsx` 的 `opens the global assistant sidebar and switches work tabs`,
仍期望“项目工作区”标题,单独复测同样失败;该侧栏处于其他会话的修改范围,本次未改动。
因此本次全量验证不能记录为全部通过。
## 2026-09-01 至 2026-09-02 实施与验证证据
- 新增 `direct-model-process-service.ts`:PowerShell/Bash/Sh 探测、工作区目录校验、96 KiB
@@ -19,7 +19,9 @@
- `ModelToolProvider` 的直连模型内置工具、浏览器、Web、知识和自定义 MCP 聚合。
- `WorkspaceAccess` 的本机和远端工作区抽象。
- 跨平台子进程树终止辅助函数。
- 面向用户专家协作的 `SubagentScheduler`、状态事件和只读模型调用。
- 面向用户专家协作的 `SubagentScheduler` 和状态事件。专家使用具备已启用本机能力的
直连模型 Runtime,继承父请求 Ask/Execute 及 authorizer,工具事件记入子任务;
Ask 保持只读,不把这些本机工具表述为远程 OpenCode 子会话。
- OpenCode、Continue 和 DeepSeek Harness 各自的 Shell/Agent 能力。
- 直连模型 `process_execute` 和 `subagent_delegate`。
- 不创建顶层 Task/Conversation 的编程 Subagent actor 与活动归并。
@@ -89,7 +91,8 @@ const processExecuteInputSchema = z.object({
}).strict()
```
- `cwd` 只接受相对工作区路径。
- `cwd` 接受绝对路径或相对工作区路径,省略时使用工作区根目录;按本机路径语义解析
`..` 和符号链接,不限制目标位于工作区内。工具 schema 向模型明确这一规则。
- 首版不接受 `env`、`stdin`、`background`、`pty`、`shell` 或任意 executable 参数。
- Shell 选择由执行后端决定,防止模型绕过平台契约启动另一套受管接口。
- 命令字符串仍可以调用当前账号本来有权运行的程序;Execute 不增加命令白名单。
@@ -114,8 +117,8 @@ type ProcessExecuteResult = {
}
```
`cwd` 返回工作区相对显示值,不把不必要的绝对用户路径送入模型上下文。Shell 在模型结果
中只返回稳定标签;绝对可执行路径只进入 Main 诊断状态。
`cwd` 在工作区内返回相对显示值,根目录为 `.`;工作区外返回规范化绝对路径,准确说明
命令实际执行位置。Shell 在模型结果中只返回稳定标签;绝对可执行路径只进入 Main 诊断状态。
### 4.3 输出边界
@@ -293,7 +296,7 @@ DeepSeek Harness 的 Main 工具代理必须使用仅包含分配 MCP 和 Web
- `failed`:启动或契约失败。
- `cancelled`:父请求取消。
结果摘要包含 Shell、相对目录、退出码、耗时和截断标记。完整有界 stdout/stderr 进入工具
结果摘要包含 Shell、工作目录、退出码、耗时和截断标记。完整有界 stdout/stderr 进入工具
输出字段,不写应用诊断日志。
### 9.2 Subagent 事件
@@ -351,7 +354,7 @@ type SubagentActor =
- 工具可见性矩阵和 Runtime target 过滤。
- Ask 伪造进程调用拒绝。
- Windows PowerShell、POSIX Bash/Sh 选择和参数。
- cwd 默认、合法子目录、越界、符号链接越界和不存在。
- cwd 默认、相对子目录、绝对路径、工作区外目录、指向外部的符号链接和不存在的目录。
- exit 0、非零退出、spawn 失败、超时、取消和输出截断。
- 环境筛选、本机工具 PATH 优先且模型凭据不进入进程。
- 子级模式、模型、项目、执行空间和能力继承。
@@ -28,7 +28,7 @@ Job、Subjob 和 Run 继续是内部对象,不创建树形 UI、独立路由
```text
编程能力
进程执行
使用当前工作区和当前用户权限运行 PowerShell/Bash 命令。
使用当前用户权限运行 PowerShell、Bash 或 Sh 命令,默认在工作区执行,也可指定其他工作目录。
编程 Subagent
将聚焦任务委派给同一模型连接;继承当前工作模式和工作区。
```
@@ -54,7 +54,7 @@ PowerShell · packages/app · npm test
```
- 命令只显示单行有界摘要,完整参数进入可展开详情。
- 目录显示工作区相对路径;工作区根显示 `.`。
- 工作区内目录显示相对路径,工作区根显示 `.`;工作区外目录显示规范化绝对路径。
- 不在摘要中显示完整环境或凭据。
### 3.2 状态
@@ -42,12 +42,13 @@ Shell、目录、退出码、输出和耗时。
Given 当前项目位于 macOS 或 Linux 本机,When 模型运行同一工具,Then GoodBuddy 使用
Bash,Bash 不可用时使用 Sh,工具契约保持不变且实际 Shell 明确可见。
### US-A3 使用工作区子目录
### US-A3 选择命令工作目录
关联:FR-3。
Given 项目包含多个包,When 模型传入工作区内相对目录,Then 命令在该目录执行;When
路径越出工作区、不是目录或不存在,Then 命令不启动并返回准确错误。
Given 当前请求为本机 Execute,When 模型传入绝对路径或相对工作区路径,Then 命令在
解析后的目录执行,包括工作区外目录和符号链接目标,And 后续省略 `cwd` 时仍使用原工作区。
When 目标不是目录或不存在,Then 命令不启动并返回准确错误。
### US-A4 根据非零退出继续修正
+7 -3
View File
@@ -1,7 +1,8 @@
# 知识库
知识库负责资料导入、解析、分块、索引、检索、图谱和带来源的知识问答,也负责将
Dify、FastGPT 和 RAGFlow 中已有的知识库接入同一检索范围。
知识库已提供本地资料导入、解析、分块、索引、检索、图谱和带来源的知识问答。
将 Dify、FastGPT 和 RAGFlow 中已有知识库接入同一检索范围仍处于规划阶段,不属于
当前已提供能力。
## 文档导航
@@ -9,6 +10,8 @@ Dify、FastGPT 和 RAGFlow 中已有的知识库接入同一检索范围。
| --- | --- |
| [知识库检索与分块增强 PRD](./prd.md) | 检索、分块、诊断和产品验收 |
| [User Stories](./user-stories.md) | 知识使用者、维护者和调试者场景 |
| [UI 设计](./ui-design.md) | 当前本地知识库的信息架构、首次使用、状态与高级功能分层 |
| [实施进度](./progress.md) | 已验证实现事实、验证命令与剩余外部知识库工作 |
| [本地文本向量技术设计](./local-embedding-technical-design.md) | 本地与兼容向量连接、模型包、索引和进程边界 |
| [知识检索评估](./retrieval-evaluation.md) | 检索质量评估方法、数据集、指标和门槛 |
| [外部知识库接入 PRD](./external-knowledge-prd.md) | 外部实例、知识库绑定、检索范围和产品验收 |
@@ -27,4 +30,5 @@ Dify、FastGPT 和 RAGFlow 中已有的知识库接入同一检索范围。
或 Agent,也不在外部实例中创建、修改、上传或删除知识库内容。远端资料不会批量同步
或进入本地索引;实际用于回答的有界片段会随引用保存在本地会话中。
当前尚未拆出覆盖整个知识库功能家族的统一逻辑、UI 设计和功能进度文档。
本目录尚未拆出统一逻辑设计;当前本地知识库状态规则由 PRD、User Stories 和实现契约
共同约束。外部知识库继续使用独立逻辑设计,直至进入实施阶段。
@@ -162,7 +162,7 @@ GoodBuddy 当前已经具备:
引用和脱敏诊断。
- Agent Runtime 的选择或切换不改变当前向量连接;向量连接的选择或切换也不改变当前
Agent Runtime。
- 未来记忆、笔记或其他本地语义检索若要复用向量连接,必须经过独立产品设计、权限与
- 分区记忆、笔记或其他本地语义检索若要复用向量连接,必须经过独立产品设计、权限与
数据路径评审后显式接入,不能因存在全局向量连接而自动获得使用权。
---
+25 -6
View File
@@ -5,8 +5,8 @@
| 项目 | 内容 |
| --- | --- |
| 状态 | 实施中 |
| 版本 | 0.2 |
| 日期 | 2026-08-22 |
| 版本 | 0.3 |
| 日期 | 2026-09-05 |
| 适用产品 | GoodBuddy 桌面端 |
| 实施范围 | 第一阶段:可用、可见、可诊断;第二阶段:可调、可优化、可维护 |
| 相关设计 | [本地文本向量模型与连接设计](./local-embedding-technical-design.md) |
@@ -149,7 +149,7 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
## 6. 信息架构
知识工作区继续使用主从布局和现有四个页签:
知识工作区使用主从布局和四个面向用户任务的页签:
```text
知识库
@@ -160,14 +160,21 @@ OpenAI 兼容向量模型、RRF 混合检索、知识图谱、任务状态和来
│ └─ 分块查看与维护
├─ 知识图谱
├─ 任务中心
└─ 设置
└─ 高级设置
├─ 问答效果入口
├─ 向量索引
├─ 检索设置
├─ 分块设置
└─ 图谱设置
```
“检索测试”是当前知识库的高频诊断操作,通过知识库标题区次操作打开独立
工作台,不新增第五个一级页签。
“检索测试”是当前知识库的高频验证操作,通过知识库标题区和“高级设置 →
问答效果”打开同一个独立工作台,不新增第五个一级页签。工作台首次打开只突出真实
问题输入和测试操作;候选倍数、权重、阈值、重排和上下文预算默认折叠。
首次使用流程固定为“创建知识库 → 添加资料 → 等待可检索 → 用于当前对话”。创建表单
默认只展示名称和描述;存储方式与知识图谱属于折叠的高级设置。没有知识库时,唯一主操作
位于首次使用空状态中。
全局向量模型仍在“设置 → 模型连接 → 向量模型”中配置。GoodBuddy 内置连接和用户添加的
OpenAI 兼容连接使用同一连接列表;其界面、数据路径及切换语义以
@@ -195,6 +202,10 @@ Tokenizer、原始向量或索引访问能力传入 Runtime。切换 Runtime 不
2. 检索方式:模型按需检索、每次先检索。
3. 当前范围为空、索引降级或向量未配置时的短说明。
知识库 ID 选择按 Conversation 持久化。新对话和未保存过知识范围的旧对话默认不选择
任何知识库;切换对话恢复各自范围,不得在应用启动时自动勾选全部知识库。新建知识库只
加入当前 Conversation,不重新启用用户已取消的其他知识库。
## 7. 第一阶段:可用、可见、可诊断
### 7.1 检索方式
@@ -296,6 +307,14 @@ Tokenizer、原始向量或索引访问能力传入 Runtime。切换 Runtime 不
- 失败文档数。
- 当前向量模型与索引是否兼容。
`indexedDocumentCount` 只统计状态为 `ready` 的文档;`queued`、`parsing` 和
`indexing` 计入处理中,`failed` 单独计入需处理。文档行持续显示全文、向量和图谱通道
状态。已有可用分块的失败文档仍允许查看分块,并提供重新处理。
文档行可通过受控 `knowledgeBaseId + documentId` 打开数据库中保存的原始来源。
Renderer 不得提交任意路径。移除来源必须先说明将删除的文档、索引、图谱证据及是否包含
应用托管副本,用户确认后才执行。
降级事件包括:
- 未配置向量模型。
+41
View File
@@ -0,0 +1,41 @@
# 知识库实施进度
## 已验证实现
截至 2026-09-05:
- 知识库选择按 Conversation 持久化,新对话默认空范围。
- 创建流程默认只展示名称和描述,高级存储与图谱设置折叠。
- 知识库汇总区分可检索、处理中和需处理文档。
- 文档行展示全文、向量和图谱状态,并支持打开原始来源和重新处理失败文档。
- 移除来源需要确认并说明级联影响。
- 检索高级参数默认折叠,“高级设置”提供问答效果入口。
- 向量未配置状态可直接前往模型设置。
- 有效 DOCX 与多页 PDF 已通过磁盘文件导入、解析、分块和检索生产链路。
## 验证证据
2026-09-05 最终验证:
- `npm test`:325 个测试文件、3511 项测试通过;8 个文件、55 项手动或集成测试按既有
配置跳过。
- `npm run typecheck`:通过。
- `npm run lint`:通过。
- `npm run build`:Electron Main、Preload、Renderer 生产构建及控制面安装包检查通过。
- `npx vitest run src/main/knowledge/knowledge-service.test.ts -t "imports real DOCX and PDF files from disk into searchable chunks"`:
有效 Open XML DOCX 与两页原生文本 PDF 均通过生产 `KnowledgeService.importPaths`
导入、解析、分块和全文检索。
2026-09-06 对话知识库选择补充验证:
- `App.test.tsx` 覆盖点击名称、文档数量和行本身时切换选中状态,以及空目标失焦时
弹层不提前卸载。Windows 隔离桌面实际点击名称后选中、点击数量后取消,弹层保持打开。
- 本轮全量校验结果见[工作栏验证记录](../assistant-workbar/progress.md);本项验证未调用模型。
## 剩余工作
- 外部 Dify、FastGPT 和 RAGFlow 知识库接入仍为规划中能力。
- 扫描 PDF 的真实 OCR 验收依赖用户已安装并校验的 OCR 模型;本次 DOCX/PDF 回归覆盖
原生文本 PDF,不宣称覆盖扫描件 OCR。
- PDF.js 在测试环境会提示未配置 `standardFontDataUrl`;文本、页码定位与检索结果不受
影响。
+70
View File
@@ -0,0 +1,70 @@
# 知识库 UI 设计
## 1. 设计目标
知识库优先帮助普通用户完成“添加资料并可靠提问”,高级 RAG 调试和图谱维护不得阻塞
首次使用。全局视觉、页面壳层、通知、焦点和危险操作遵循
[UI-DESIGN.md](../../../UI-DESIGN.md)。
## 2. 信息架构
```text
知识库
├─ 知识库列表
└─ 当前知识库
├─ 文档与来源
├─ 知识图谱(启用后显示)
├─ 任务中心
└─ 高级设置
```
标题区提供“用于当前对话”“检索测试”、编辑和删除。新建知识库是页面主操作。
“高级设置”承载问答效果入口、向量索引、分块与图谱配置,不把工程参数放在首次路径。
## 3. 首次使用
没有知识库时,页面空状态显示唯一的“新建知识库”主操作。创建表单默认只展示名称和
描述;存储方式与知识图谱放入折叠的“高级设置”。创建完成后进入“文档与来源”,用户
可以导入文件、文件夹或 URL。
文档处理完成后,标题区“用于当前对话”可用。该操作把当前知识库加入当前
Conversation 的检索范围,返回对话并聚焦输入框。
## 4. 状态
知识库列表显示“可用”和“需处理”文档数。详情标题持续显示可检索、处理中、需处理和
总文档数,即使用户填写了知识库描述也不隐藏状态。
文档行显示:
- 总体处理状态。
- 全文状态。
- 向量状态。
- 启用图谱时的图谱状态。
失败文档保留错误信息和“重新处理”。只要仍有可用分块,就允许打开分块管理,避免隐藏
上一版可用索引。
## 5. 问答效果与高级参数
“检索测试”默认只突出真实问题、测试按钮和结果。高级参数使用折叠区,包含候选倍数、
通道权重、最低相似度、重排、结果数和上下文预算。“高级设置 → 问答效果”打开同一
工作台。
向量模型未配置时显示“前往设置”,直接进入模型连接,不使用只有说明文字的死路状态。
## 6. 来源操作
文档行提供“打开来源”,Main 根据知识库和文档归属重新读取保存的文件路径或 URL。
移除来源使用确认对话框,说明文档数、索引、图谱证据和托管副本影响。初始焦点位于
取消,提交期间禁止重复操作,失败时保留对话框和错误。
## 7. 对话范围
知识库多选属于 Conversation。新对话默认空范围;切换对话恢复各自选择。创建新知识库
只加入当前 Conversation,不重新启用其他已取消项。发送请求时冻结当前选择和检索方式。
输入区的知识库选择弹层使用整行原生 Checkbox 标签,点击名称、文档数量或行内空白
均切换一次选择;选中背景和整行焦点环保持可见。Tab 聚焦、Space 切换,Escape 关闭并
返回触发按钮。点击行内非焦点元素时产生的空目标失焦不关闭弹层;点击外部或焦点明确
移到外部时才关闭,避免原生标签完成勾选前被卸载。
@@ -33,6 +33,18 @@
## 2. Epic A:明确控制是否检索
### US-A0 首次建立可用知识库
作为首次使用者,我希望先命名知识库并添加资料,不需要先理解向量、分块或图谱。
验收:
- 没有知识库时只显示一个“新建知识库”主操作
- 创建表单默认只展示名称和描述
- 存储方式和知识图谱位于折叠的高级设置
- 文档可检索后提供“用于当前对话”
- 高级检索参数不阻塞首次导入和首次提问
### US-A1 模型按需检索
作为普通知识使用者,我希望保留由模型判断是否需要检索的模式,以便一般闲聊
@@ -73,6 +85,9 @@
- 同一请求最多启用 20 个知识库
- 对话输入区持续显示已选数量和检索方式
- 范围为空时检索方式不产生误导状态
- 知识库 ID 与检索方式一起按 Conversation 持久化
- 新对话和未保存过范围的旧对话默认不勾选知识库
- 切换 Conversation 恢复各自选择,不共享临时全局范围
## 3. Epic B:检索可见、可诊断
@@ -233,6 +248,18 @@
- 全库重建可取消
- 已成功替换的文档继续可用
### US-D4 查看和移除来源
作为知识库维护者,我希望打开原始文档并在移除来源前看到准确影响。
验收:
- 文档行可通过系统默认应用打开数据库保存的原始来源
- Renderer 只能提交知识库 ID 和文档 ID,不能提交任意路径或 URL
- 移除来源前显示来源名称、文档数量以及索引和图谱证据影响
- 托管模式额外说明应用托管副本会被删除
- 用户确认前不得删除来源
## 6. Epic E:高级分块
### US-E1 固定分块
+12 -11
View File
@@ -5,8 +5,8 @@
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.3 |
| 日期 | 2026-08-19 |
| 版本 | 0.4 |
| 日期 | 2026-09-05 |
| 依赖 | [自动化平台总体设计](../../architecture/automation-platform-architecture.md)、[智能心跳 PRD](../smart-heartbeat/prd.md)、[Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) |
## 1. 背景
@@ -16,13 +16,13 @@ GoodBuddy 当前记忆已经支持:
- `global`、`project`、`conversation` 三种作用域。
- `preference`、`fact`、`summary`、`procedure` 四种类型。
- `proposed`、`confirmed`、`rejected` 三种状态。
- 智能心跳提出 Global 或 Project 记忆候选,由用户确认;其长期“未来分区记忆”方向尚未设计。
- 智能心跳提出 Global 或 Project 记忆候选,由用户确认;后续按本 PRD 的分区记忆模型增强。
但当前能力仍不足以支撑自动化和并行实验:
1. 交互请求会把已加载列表中的最多 20 条已确认记忆直接拼入提示,缺少查询相关度和明确的
会话级过滤契约。
2. 数据库有会话作用域,但旧版心跳候选与未来唤起需求混在同一产品概念中。
2. 数据库有会话作用域,但旧版心跳候选与分区记忆的唤起需求混在同一产品概念中。
3. 缺少 Automation、Experiment 和 Run 分区。
4. 来源字段存在于表结构,但普通创建和心跳候选尚未完整保存来源关系。
5. 缺少事实的有效时间、冲突、替代、访问记录和衰减。
@@ -64,10 +64,11 @@ SQLite 显式字段、FTS、来源关系和可选本地 Embedding 足以支持
- 同一实体跨大量会话的别名消歧。
- 可解释的关系证据链。
### 2.5 未来分区记忆尚待设计
### 2.5 分区记忆与唤起
已确认智能心跳的长期方向是“未来分区记忆”,但当前尚未定义其数据结构、唤起条件、状态、
生命周期、与长期记忆的关系或迁移方式。本 PRD 不新增 `FutureMemory` 类型、表或检索规则。
正式产品概念统一为“分区记忆”。时间和内部事件可以作为分区记忆条目的可选唤起条件,但
不能改变条目所属分区、绕过分区读取边界,也不建立平行的记忆类型或表。唤起字段、状态、
生命周期、页面和旧数据迁移必须在实施前补充设计。
## 3. 目标
@@ -430,8 +431,8 @@ Project 记忆与 Global 偏好冲突时:
每条记忆展示内容、类型、范围、来源、状态、时间、置信度、重要性和冲突。
智能心跳的完整配置仍在“智能心跳”菜单中管理;未来分区记忆完成独立设计前,不加入记忆
中心信息架构。
智能心跳的完整配置仍在“智能心跳”菜单中管理。分区记忆的唤起条件完成字段、状态和生命周期
设计后,可以在记忆中心增加“待唤起”视图;不得创建平行的记忆分区或内容页。
## 16. 数据模型建议
@@ -474,13 +475,13 @@ Project 记忆与 Global 偏好冲突时:
6. 增加冲突、时态、替代和归档。
7. 增加实验冻结快照与 Run 隔离。
8. 增加可选本地 Embedding 和混合排序。
9. 未来分区记忆和时间知识图谱都必须在明确需求与独立设计后再实施。
9. 增加分区记忆的时间/内部事件唤起;时间知识图谱仍须在明确需求与独立设计后再实施。
## 19. 验收标准
- [ ] 普通会话只读取 Global、当前 Project 和当前 Conversation 的允许记忆。
- [ ] 智能心跳配置只能属于 Global 或 Main 已验证的一个、多个 Project。
- [ ] 未来分区记忆完成独立设计前,不新增相关表、状态或检索行为。
- [ ] 唤起条件始终依附于一个已验证的记忆分区,不建立平行的记忆表或范围。
- [ ] Task 执行只读取内部 Run 快照绑定的分区。
- [ ] 实验 Run 不能读取其他 Run 的消息或记忆。
- [ ] 每条非手动记忆都有可追溯来源。
+2 -1
View File
@@ -11,7 +11,8 @@
## 1. 背景
GoodBuddy 已能把一个请求并行委派给最多三个只读专家,再综合结果。这适合“一次请求,
GoodBuddy 已能把一个请求并行委派给最多三个专家,再综合结果;专家继承 Ask/Execute
并使用本机直连模型工具,Ask 仍只读。这适合“一次请求,
多种专业视角”,但不等同于实验:当前没有结构化变量、重复运行、统一指标、结果晋升和
Run 级记忆隔离。
@@ -102,7 +102,7 @@ bootstrap 能力元数据。公开直连能力仍须完成本文末尾 GitHub/
探测在 Host Key 已固定且 SSH 认证成功后执行,只检查当前选择的“关于与更新”来源:
- Host 是受支持的 Linux `x64` 或 `arm64`。
- Host 是 Linux `x64`、Linux `arm64` 或 macOS `arm64`;Intel Mac 不在支持范围。
- SSH 账号 Home 可写,GoodBuddy-owned staging 可创建。
- 可用空间满足签名目录声明的包大小、解包上限和安全余量。
- Host 可以通过 HTTPS 访问所选 GitHub 或北京镜像 URL。
+121 -6
View File
@@ -12,7 +12,7 @@ Linux x64/arm64、取消和离线 GoodBuddy 传输的真实 Host 验收。
Windows 到 Linux x64 的安装、Agent-owned Prompt、Agent 本地模型 gateway、断线恢复、
同一 OpenCode Session 续接、取消和终态清理已经使用真实模型与工具验证。Agent
`0.11.14` 已通过独立 workflow 发布 Linux x64/arm64 复合包和签名累计目录;当前未发布
Agent 源码 lock 为 `0.11.18`,Desktop 候选为 `0.12.4`,等待各自独立发布审批。
Agent 源码 lock 为已发布的 `0.11.21`,Desktop 候选为 `0.12.8`;本次桌面打包修复不重发 Agent。
现有源码显示本地与远端 OpenCode 原生 Task,并取消 GoodBuddy 对生产 Prompt 的
固定墙钟总时限。失败的 `agent-v0.11.3` 保持不可变且未发布。
@@ -69,6 +69,10 @@ Detached GoodBuddy Agent
## Agent 安装与生命周期
- Agent bundle 通过 manifest、Ed25519 签名、payload digest、平台和架构校验。
- 外层 `agent-package.json` 描述符上限为 4 MiB,覆盖包含 OpenCode 离线依赖的实际包
(2026-09-08 Linux x64 验证包为 1,101,863 字节)。Desktop 解包、Host prepare/commit
与目录生成器使用相同上限;其余元数据仍为 1 MiB,签名、文件数量、归档大小和完整性校验不变。
- Runtime 组包使用的 OpenCode 离线依赖缓存按插件版本复用,安装与替换规则见 [Runtime 资源](../../../BUILD.md#runtime-资源)。复制进 Runtime 包的依赖仍属于签名 manifest 的 payload。
- 安装使用当前 SSH 用户目录中的 GoodBuddy-owned 路径和 side-by-side digest 目录。
- Agent 代码与固定 Node Runtime 一起位于签名 compound payload。显式安装或重装从本次
已完整校验的准备目录原子发布整个 Agent,不扫描旧安装、不创建跨版本 Node 硬链接;
@@ -94,6 +98,9 @@ Detached GoodBuddy Agent
- Agent 是按需启动的 detached process,不注册开机服务,不依赖 systemd、D-Bus 或 Linger。
- 每个模型桥 helper 都为 loopback HTTP 入口生成一次性随机路径 capability;只有写入当前 OpenCode 子进程配置的 URL 可以访问该入口,其他本机用户即使发现临时端口也不能提交模型请求。
- Agent 监听当前用户拥有的私有 Unix socket。SSH 中断只关闭 relay,不拥有 Agent 和活动 Runtime 的生命周期。
- 私有握手的 HMAC 响应按固定 32 字节的规范 Base64URL 校验,不套用要求字母数字开头
的应用 ID 规则;health nonce 使用固定前缀。读取握手时,流结束或关闭都立即失败,
不留下未结算 Promise;Host Key、同 UID 校验和 HMAC 验证保持不变。
- SSH Host 设置通过固定探针和有界 SFTP 显示 Host 已登记的 Agent/OpenCode 版本,并与当前
GoodBuddy 所需版本比较。进入或切回该设置页只读取本地 Host 列表,不连接任何 Host;
用户点击“刷新版本”后才探测对应 Host,探测不安装或切换远端组件。项目切换与此按钮
@@ -104,6 +111,11 @@ Detached GoodBuddy Agent
小型签名目录。Renderer 只得到本地/在线版本、是否有更新、架构、远端 Runtime 版本、
协议和本地状态,不得到缓存路径、key ID 或 digest。目录检查不下载 `.gbagent`,在线
包下载只由用户点击带目标版本的操作触发;离线导入/导出通过 Main 管理的文件对话框完成。
- GitHub 来源按每页 10 条读取 Release 元数据,最多检查原有的 100 条历史发布,
找到首个有效 Agent 目录及签名地址后立即停止。每次响应仍保留 1 MiB 上限,
避免一次读取全部历史正文和资产详情导致目录检查失败;签名、URL 与包校验不变。
2026-09-06 使用当前 Main 目录读取实现验证公开 GitHub 与北京镜像,两者均可读取
Agent `0.11.19` 的 Linux x64、Linux arm64 和 macOS arm64 条目,未下载或安装包。
- “自动”只在 operation/prepare 前用 Host capability probe 选择 acquisition:直连明确
可用才选 Host 下载,否则选 GoodBuddy 传输;显式选择不被改写。prepare、commit 或
adoption 失败后都不跨 acquisition 自动 fallback。
@@ -190,6 +202,10 @@ Ask 与 Execute 一样直接启动已签名 Runtime,不要求 Host 安装额
- Ask 启动时同时注入 OpenCode 顶层和 build Agent 的 `permission: "ask"`;Agent
工具权限分发边界只批准上述原生读取请求。该双重边界不另加文件系统 confinement,
也不改变 Runtime 的直接启动方式。
- Execute 的直接启动与模型桥启动均显式设置顶层和 build Agent 的 `permission: "allow"`,
避免继承 OpenCode 默认的 `external_directory` 询问;项目目录只是默认工作目录。
原生子代理同样使用当前 SSH 账号可访问的路径。仍出现的 ACP 权限请求由 Agent 按
当前 Prompt 模式自动答复,不增加 Desktop 人工审批,也不依赖日志解析。
### Execute
@@ -233,7 +249,25 @@ Execute 直接启动已签名 Runtime:
- OpenCode 原生 Task 工具按 `subagent_type`、`description`、`prompt` 和稳定 tool call ID
解析为子 Agent 事件。本地 OpenCode SDK 与远端 ACP 增量工具事件复用同一转换;ACP
首帧缺少参数时可以先显示普通工具活动,后续参数确认其为 Task 后必须替换为子 Agent
状态卡,并持久化任务说明、终态与输出。
状态卡,并持久化任务说明、终态与输出。卡片展开后分为“执行过程”和“最终结果”:
正文、折叠思考与可展开工具记录按收到的顺序显示,工具状态原位更新;完成后仅在最终
结果区显示与末尾正文完全相同的返回结果,避免重复。失败或取消保留已有过程,未终结
工具不再显示进行中。
- 本地 SDK 通过 Task `metadata.sessionId` 关联子会话,复用稳定消息块 ID 保存过程。
固定 OpenCode 1.18.29 的 ACP 不转发子会话,因此 Agent 的模型桥 helper 在受管临时目录
写入事件插件,并通过既有 ACP `session/update` 的 `_meta.goodbuddySubagentEvent`
转发到所属 Task;不新增端口,不修改签名 Runtime 包。插件文件随 helper 退出清理,
事件继续经过既有 Agent transcript、ACK 和 Desktop 消息持久化路径。
- ACP 工具结果同时支持文本 `content` 和 `rawOutput.output`,移除 OpenCode 的
`<task><task_result>` 外层包装后渲染正文。恢复已有子代理时使用 ACP 所带的所属
`toolCallId` 继续路由,不等待新的父 Task metadata,也不重放模型请求。
远端实时过程需要包含该插件的 Agent;旧 Agent 的最终结果仍可查看,历史未采集过程
不会被补造。
- 本机 OpenCode SDK 路径允许不同会话并行,同一会话仍按请求顺序执行。每个请求独立拥有
SSE 事件订阅,并在正常完成、错误、取消或消费方结束迭代时主动关闭自己的响应流,
不关闭其他会话的订阅或共享 Server。取消导致事件迭代结束时保留原取消原因,不改报
“事件流意外结束”。这一清理规则属于本机 SDK 路径,不改变远端 Agent 持有 ACP Prompt
的断线继续执行语义。
- 同一 detached Agent 存活时,短暂 SSH 断线依次执行 `controller/resume`、`runtime/resumeAcpChannel` 和 `runtime/replayAcpChannel`,从 Main 已确认的 cursor 后只重放 Agent 到 Main 的已记录输出。重复 frame 会被确认但不会再次交给上层。若上一代连接只留下没有活动请求的 detached binding,完成精确 controller takeover 后会先有界停止并核对遗留 Runtime process,再用新 channel epoch 重新打开同一 binding 并恢复已有 ACP session;其他 controller、未证明 takeover 或仍有活动请求的 binding 仍被拒绝。
- Main 到 Runtime 的 ACP 输入、模型请求、工具请求和 blob 不自动重放。
- 无法确认外部 Provider 是否已处理的模型调用保持结果未知,避免重复计费或重复副作用。
@@ -349,12 +383,92 @@ model bridge,以及生命周期和恢复逻辑。单元测试、mock、fixture
不可达,应立即把真实 Host 验证报告为开发阻塞,保持该验证未完成,不能把 Agent 改动表述
为已完成,也不能把首次真实验证推迟到发布阶段。
## 离线配置缓存简化验证(2026-09-08)
`npm test -- tests/opencode-config.test.ts src/main/agent/opencode-runtime.test.ts src/main/agent/bundled-runtimes.test.ts tests/remote-runtime-bundle.test.ts tests/agent-bundle.test.ts --testTimeout=30000` 通过 108 项;`npm run typecheck`、`npm run lint` 和 `npm run build` 通过。
全量 `npm test -- --testTimeout=30000 --hookTimeout=120000` 当次结果为 3556 项通过、28 项失败、58 项跳过;其中离线配置路径断言随后修正并通过上述聚焦测试,未重跑全量。其余失败涉及 Agent 包元数据大小限制、Darwin 组包超时、Runtime 探测、DSH 和打包夹具。
测试 Host 的 SSH 端口可达,但候选复合包验证受元数据大小限制阻塞,未部署或执行实机安装/更新验收;本次真实模型调用 0 次。
以上为缓存简化阶段的历史记录,不代表后续发布前修复的最终结果。
## 发布前修复实机验证(2026-09-08)
共享 Linux x64 Host 上使用当前源码、锁定 Node 与 OpenCode `1.18.29`、进程内临时签名
身份组包,签名目录读取、生产 `preparePackage/commitPackage`、adopt、Runtime activate
与 health 通过。所有写入位于独立 GoodBuddy 测试 HOME,未修改共享安装。
真实 Desktop 协议客户端已通过 Ask 文本回复、Execute 原生 Task 子代理在 Workspace 外
的专用测试目录写文件、读回校验、关闭连接后重连同一 daemon,以及 stop/bootstrap 后
health。首轮发现握手 Base64URL 误拒绝后完成上述修复;最终源码复测通过,独立 Unix
endpoint 连续 20 次握手也通过。两轮模型账本各 5 次 completed,本轮合计 10 次真实文本
模型调用,没有发布工件、签名目录或 release tag。
最终自动化回归与尚未覆盖的系统休眠场景见
[工作栏进度](../assistant-workbar/progress.md)。
## 已完成的 E2E 验收记录
### macOS arm64 Host 支持
`agents.yml` 增加 `macos-15` / `darwin-arm64` 的原生 CI 组包任务,使用临时测试
签名校验 Agent、固定 Node、Koffi 和 OpenCode,并比较两次组包的字节摘要。
构建输入与检查的权威说明见 [BUILD.md 的 Agent 原生 CI 策略](../../../BUILD.md#agent-原生-ci-策略)。
此 CI 任务不发布工件。生产 lock、`agent-release.yml`、安装器、Desktop 包管理和
SSH Host 链路现支持 `linux-x64`、`linux-arm64`、`darwin-arm64`,三个目标共用
原有 catalog JSON 和签名格式,不增加 Mac 专用目录。旧版 Linux-only Desktop
读取器不能解析混合目录,首次发布 Darwin 条目前需先升级 Desktop。
macOS 保持 SSH 按需启动 detached Agent,不要求 launchd、开机服务或管理员权限。
私有 socket 使用 `/private/tmp`,通过 `getpeereid` 核对 UID;进程身份、启动标识和
进程组通过系统原生 API 获取。取消前枚举 Runtime 的实际后代并核对 PID、启动时间
和可执行文件,停止独立进程组中的工具,再停止 Runtime,避免只杀主进程组留下工具。
2026-09-06 当前源码在真实 macOS arm64 Host 的隔离目录通过生产安装器
`preparePackage` / `commitPackage`、adopt、health、Runtime activate、stop、
bootstrap 和再次 health。实际 Desktop 协议客户端通过 Attach、只读 Workspace
打开、真实 Ask 输出、Execute 写入并读回测试文件、取消工具并验证子进程退出,以及
重新连接同一 daemon。两个 Darwin 原生进程测试通过,其中一个覆盖独立进程组子进程。
共享 Linux x64 Host 同时通过当前源码组包、生产安装、生命周期、真实 Ask 和重连。
这些验证没有发布 catalog 或工件;GitHub Actions 和正式下载渠道的发布验收尚未执行。
最终本地 typecheck、lint、开发 build 和 diff whitespace 检查通过。最终全量
`npm test` 为 3520 通过、57 跳过、0 失败(326 个测试文件通过、9 个跳过)。
此前 ActivityPanel 的批量记录用例超时、App 的实时浏览器用例未观察到预期调用,
分别复跑后通过;未修改这两个用例或放宽超时,随后完整测试套件通过。
隔离测试 ledger 统计 Mac 模型请求 7 次,Linux 首轮
1 次;最终 Linux 源码重装后又完成 1 次最小 Ask,总计 9 次真实模型请求。
以下为较早的仅构建阶段记录,摘要不代表上述最终源码产物:
2026-09-06:在真实 macOS 26.5.2 / arm64 Host 的独立测试目录运行当前源码
`build/agent-ci-bundle.cjs --platform darwin --arch arm64`,退出码为 0。
测试使用固定 Node 24.19.0、OpenCode 1.18.29 和内存临时签名密钥;Node 官方
归档 SHA-256、OpenCode integrity、Mach-O 架构、内外层签名和两次组包摘要比较
通过,Agent CLI 加载、Koffi 原生模块加载与 OpenCode `--version` 检查通过。
生成 `goodbuddy-agent-0.11.18-darwin-arm64.gbagent`,本次测试包 SHA-256 为
`9c63ba080def56ebd371a622f2650b89d0c652444077bd6ee19c4bd51b60f6ff`。
临时签名每次独立运行都会变化,因此该摘要只标识本次测试产物,不是发布输入。
源码压缩传输 529,624 字节,大依赖由 Host 直接下载并复用 npm 缓存。
真实模型调用为 0 次;没有安装 Agent 服务,没有修改在线 catalog 或发布工件。
该仅构建阶段的聚焦测试为 53 项通过、1 项平台跳过,typecheck、lint 和开发生产
bundle 构建通过;这组历史结果不替代完整 Host 支持改动后的最终验证。
### Linux 历史验收
- 2026-09-06 使用当前源码隔离 Agent 验证原生子代理过程插件。最终场景收到 37 条 ACP
插件事件,桌面侧从已保留活动重建跟踪器后仍接收后续文本与工具;子代理和父请求正常
完成,最终模型调用账本为 4 次 completed。此前成功场景另 4 次,两轮真实 Host 共
8 次模型请求。
- 同日 Windows 隔离桌面实际发送 OpenCode 原生 Task,展开卡片可看到进行中的正文与
read 工具,取消后已有过程保留且工具显示已取消;正确工作区场景完成后显示测试文件
内容,最终结果不重复留在过程末尾。本机有界代理共记录 10 次模型请求(含错误路径、
取消和成功场景);另外 2 次使用原生测试配置的请求返回 401,改选显式测试模型后通过。
未改动正常用户设置,也未发布 Agent 包。全量静态与测试结果见
[本轮验证记录](../assistant-workbar/progress.md)。
- 2026-08 的本地 fixture 完整验证 Linux x64 Agent `0.11.2-e2e.12`、Node `24.19.0`
和 Agent protocol `2.0`;当时没有 arm64 fixture,因此该记录不能作为当前独立发布
的双架构验收。Agent `0.11.10` 后续已由原生 workflow 发布并公开验证双架构工件;
当前源码 lock 固定为 `0.11.18`;正式发布状态以独立 Agent Release 与签名 catalog 为准。
当前源码 lock 固定为 `0.11.21`;正式发布状态以独立 Agent Release 与签名 catalog 为准。
- 2026-08-30 在共享 Linux x64 Host 的隔离测试 HOME 中验证当前 `0.11.13` 源码候选:
签名 Runtime 清单的测试墙钟上限为 1 秒,模型桥首轮故意延迟 2.515 秒后 Prompt 仍在
5.770 秒正常完成;随后 OpenCode 原生 Task 依次产生 `running`、`completed` 子 Agent
@@ -372,7 +486,7 @@ model bridge,以及生命周期和恢复逻辑。单元测试、mock、fixture
- 最终源码复核后重新构建测试签名 package:
SHA-256 `39307fd1dccd638dcb723027591bcd83deab7057d550c99eff6f037e339829f4`,
installation `agent-2cad05acfebe853394807e3a11f7befcd9f47215c2cf3a33a03d5f644b45716e`,
Runtime `1.18.9` / bundle
Runtime `1.18.29` / bundle
`sha256:6c7fc975415ef2547d3729bc4772d3e78362c2e76c05d1fc24968a0468fcb7ba`。
最终 15 秒 detach/recovery 使用两个模型轮次、一个工具和一个终态,ledger 两轮均
`completed + delivered`,语义 `latest=ACK=41`,保留事件、Runtime owner、ACP channel/
@@ -418,7 +532,7 @@ model bridge,以及生命周期和恢复逻辑。单元测试、mock、fixture
UTF-8 字符前回退并显示 262,143 bytes;继续加载后完整追加剩余文本,页面不再显示加载
按钮且没有错误。测试未调用模型;专用远端文件、目录和本机隔离用户数据均已清理。
- 正常 Host 更新路径把 Linux x64 Host 的 Agent 更新为 `0.11.2-e2e.12`,并确认
OpenCode Runtime 已安装版本与所需版本均为 `1.18.9`。
OpenCode Runtime 已安装版本与所需版本均为 `1.18.29`。
- 一条新的 Ask 用户操作只提交一次。OpenCode 先在 build 模型轮次请求一个原生
`read`,读取专用测试 Workspace 中的证据文件,再在第二个 build 模型轮次生成最终
回答;没有 title 模型轮次,也没有第二个工具调用。
@@ -524,7 +638,8 @@ goodbuddy-agent diagnostics --installation-id <installationId>
1. `npm test`
2. `npm run typecheck`
3. `npm run lint`
4. `npm run build`
4. 发布候选的生产构建由 main 分支 CI 验证;发布前不在本地执行生产构建或打包,详见
[发布手册](../../development/release-runbook.md)。
5. 用测试签名复合包验证在线下载源选择和离线导入/导出;正式 Agent 候选还需公开校验
双架构 production 包与签名目录。
6. 在已固定 Host Key 的 Linux x64 测试 Host 上验证 attach-or-bootstrap。
-29
View File
@@ -1,29 +0,0 @@
# ShareServer
ShareServer 是 GoodBuddy 设备共享网络的独立 Web 服务。它为一个或多个组织提供身份与设备
目录、能力发布、授权策略、任务路由、可选数据中继、能力包更新、网关联邦和审计管理。
ShareServer 不属于 Electron Main,不负责桌面端局域网发现,也不自动取得设备上的能力、
文件、凭据或 SSH Host。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [PRD](./prd.md) | 管理角色、产品范围、功能需求和验收 |
| [功能逻辑](./logic-design.md) | 组织隔离、设备、发布、策略、任务和联邦状态规则 |
| [管理界面设计](./ui-design.md) | ShareServer Web 控制台的信息架构与交互 |
| [技术设计](./technical-design.md) | 服务模块、API、数据、部署、运维和验证 |
| [共享网络总体设计](../../architecture/share-network-architecture.md) | 跨端身份、能力、授权、任务、更新和协议权威 |
当前没有单独的 User Stories:管理员、能力发布者、审计员和普通成员场景已进入 PRD 与逻辑
设计。服务尚未实施,因此不创建 `progress.md`。
## 角色
- **系统管理员**:维护实例、身份源、域名、证书、存储和全局限制。
- **组织管理员**:管理本组织成员、设备、组、策略、能力和联邦关系。
- **能力发布者**:在获准范围内发布设备能力或签名能力包。
- **审批人**:处理调用、发布、更新和联邦申请。
- **审计员**:只读查看和导出获准范围内的审计事件。
- **成员**:主要通过 GoodBuddy Desktop 使用能力,只在 Web 端处理个人设备和审批。
-209
View File
@@ -1,209 +0,0 @@
# ShareServer 功能逻辑设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [ShareServer PRD](./prd.md) |
| 共享规则 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
## 1. 核心不变量
### INV-1 组织隔离先于角色
任何查询和变更先绑定 organization,再计算角色。系统管理员的跨组织操作使用独立系统管理
入口并形成审计,不能依靠省略 organization 条件获得全局数据。
### INV-2 注册不等于发布
设备注册只建立身份和连接资格。没有有效 Publication 时,目录中没有该设备能力。
### INV-3 目录不等于授权
看到能力不能推导可调用或可下载。创建任务和下载包时重新计算当前授权。
### INV-4 服务端授权不能扩大端点授权
ShareServer 的 `allow` 只是签发任务资格。Provider 和调用方仍执行各自本地策略;任一端拒绝
即终止动作。
### INV-5 策略拒绝优先
同一动作命中多个规则时使用 `deny > require_approval > allow`。没有匹配 allow 时默认 deny。
### INV-6 联邦为双边交集
单方创建或接受的关系不生效。双方 active 且两端发布、Grant 和数据策略均允许时才能跨组织
创建任务。
### INV-7 任务提供者不可改写
ShareServer 只路由调用方冻结的 provider,不在离线、繁忙或失败时替换节点。
### INV-8 不恢复短期权威
备份恢复不恢复 Web 会话、设备 socket、一次性挑战、审批 nonce 或任务数据通道。终端必须
重新连接并证明身份。
### INV-9 初始化入口一次性关闭
初始化只在实例尚无系统管理员时可用。首个系统管理员创建成功后,后续请求即使持有旧的
一次性引导材料也不能再次进入初始化流程。
## 2. 主要状态
### 2.1 成员
```text
invited -> active -> suspended -> active
invited -> expired | revoked
active/suspended -> removed
```
`suspended` 不能登录或创建新任务;历史归属保留。`removed` 不级联删除审计和已完成任务。
### 2.2 注册设备
```text
pending -> active <-> offline
pending -> expired | rejected
active/offline -> revoked
```
客户端心跳只决定 `active/offline`,不能清除 `revoked`。重新注册生成新设备 registration。
### 2.3 Publication
```text
draft -> pending_approval -> active
draft/pending_approval -> rejected
active <-> paused
active/paused -> revoked
```
provider 离线不会把 Publication 变为 paused;目录同时展示“已发布”和“当前离线”两个事实。
### 2.4 审批
```text
pending -> approved | rejected | cancelled | expired
```
审批对象内容摘要、申请主体或策略版本变化后,原审批不能复用。approved 只产生其声明范围内
的 Grant 或一次性决定。
### 2.5 Package Release
```text
draft -> verifying -> available -> paused -> available
verifying -> invalid
available/paused -> revoked
```
发布后的版本和字节不可变。修复内容必须使用新版本;revoked 不删除审计证据和对象摘要。
### 2.6 FederationTrust
```text
draft -> offered -> mutually_confirmed -> active
offered -> rejected | expired
active <-> paused
任意非终态 -> revoked
```
`mutually_confirmed` 只表示身份确认完成;双方策略同步成功后才进入 active。
## 3. 授权计算顺序
1. 验证请求身份、会话 audience 和 organization。
2. 验证用户、设备、provider、Publication 和 FederationTrust 当前状态。
3. 校验能力版本、动作、输入 schema、数据类别、大小和委托链。
4. 收集调用方组织、提供方组织、Publication 和 Grant 规则。
5. 按拒绝优先合并,计算 `deny/require_approval/allow`。
6. 校验次数、并发、预算和有效期。
7. 返回决定、稳定原因和策略引用;需要批准时建立内容绑定申请。
8. 创建任务时再次计算,并把决定快照写入任务。
目录查询可以预计算“可能可用”,但不能替代第 1 至 8 步。
## 4. 组织角色边界
| 动作 | 系统管理员 | 组织管理员 | 发布者 | 审批人 | 审计员 | 成员 |
| --- | --- | --- | --- | --- | --- | --- |
| 实例设置 | 是 | 否 | 否 | 否 | 只读有限项 | 否 |
| 成员与组 | 紧急管理 | 是 | 否 | 否 | 只读 | 自己 |
| 注册设备 | 只读/撤销 | 是 | 自己 | 否 | 只读 | 自己 |
| 发布能力/包 | 否 | 是 | 授权范围 | 否 | 只读 | 否 |
| 编辑策略 | 否 | 是 | 否 | 否 | 只读 | 否 |
| 处理审批 | 否 | 可配置 | 否 | 授权范围 | 只读 | 自己的申请 |
| 联邦关系 | 实例级许可 | 是 | 否 | 可共同审批 | 只读 | 否 |
| 审计导出 | 实例级 | 组织级 | 自己相关 | 自己相关 | 授权范围 | 自己相关 |
实际权限仍由策略计算,表格定义产品默认职责而非硬编码角色名称。
## 5. 任务路由决策
| 条件 | 结果 |
| --- | --- |
| provider 在线且双方可直连 | 签发短期直连协商信息 |
| 无法直连且双方策略允许中继 | 建立有界中继通道 |
| 中继关闭或任一方禁止 | 创建失败,说明无可用路由 |
| provider 离线 | 不排队到无限期,按能力配置有界等待或拒绝 |
| provider 接受后连接中断 | 保留 task,等待同一 provider 恢复/查询 |
| provider 终态未知且恢复期结束 | `outcome_unknown` |
| 调用方取消 | 转发取消;provider 确认后终态 cancelled |
中继容量不足只产生排队或资源错误,不切换 provider,不改变是否允许中继。
## 6. 更新策略优先级
```text
紧急撤销
> 组织强制固定版本/禁止版本
> 设备组发布通道
> 用户允许的版本范围
> 当前已安装版本
```
任何权限扩大都从自动路径转为 `require_approval`。降级只有在目标版本仍被签名目录允许且
策略明确指定时才提供,不自动视为故障回滚。
## 7. 联邦可见性
联邦目录只导出对方明确允许的能力投影:组织显示名、能力、版本范围、数据政策和可用性摘要。
不导出内部设备名、成员清单、组结构、节点地址、内部策略全文或审计记录。跨组织请求使用
双方关联 ID,双方各自解析本地主体。
## 8. 删除、撤销与保留
- 删除组织前必须停止新任务、撤销联邦和设备,并按保留策略归档审计;不提供无提示级联。
- 删除用户不删除其历史动作,审计显示不可变主体摘要。
- 撤销设备、Publication、Grant、Package 或联邦后立即阻止新令牌。
- 对象存储中的未完成任务正文按任务期限清理;审计元数据按组织保留策略清理。
- 法定保留只锁定匹配审计事件,不使被撤销身份重新可用。
## 9. 需求追踪
| 需求 | 主要逻辑 |
| --- | --- |
| `FR-1` | `INV-1`、`INV-9` 和系统管理员角色边界 |
| `FR-2` | 组织隔离不变量、成员状态和角色边界 |
| `FR-3` | 注册不等于发布、设备状态和短期权威恢复规则 |
| `FR-4` | 注册、Publication、目录与授权不变量 |
| `FR-5` | 拒绝优先不变量和授权计算顺序 |
| `FR-6` | 审批状态、内容绑定和授权重算 |
| `FR-7` | provider 冻结不变量和任务路由决策 |
| `FR-8` | Package Release 状态和更新策略优先级 |
| `FR-9` | 联邦双边交集、FederationTrust 状态和联邦可见性 |
| `FR-10` | 组织隔离、删除撤销与保留规则 |
| `FR-11` | 组织角色边界及各对象状态;展示行为由 UI 设计定义 |
| `FR-12` | 短期权威恢复规则和逻辑完整性;部署结构由技术设计定义 |
## 10. 逻辑完整性
本设计覆盖实例、组织、成员、设备、发布、授权、审批、任务、更新、联邦、撤销和恢复。实现
前仍需确定首个 OIDC 提供者矩阵、默认审计保留期、部署规模目标和中继带宽上限;这些部署
参数应可配置,不改变拒绝优先、双边联邦和 provider 冻结规则。
-149
View File
@@ -1,149 +0,0 @@
# ShareServer PRD
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 适用产品 | 独立部署的 ShareServer 与 Web 管理控制台 |
| 关联架构 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
| 关联文档 | [功能逻辑](./logic-design.md)、[管理界面](./ui-design.md)、[技术设计](./technical-design.md) |
## 1. 背景
局域网配对适合附近设备,但不能统一解决跨网络访问、成员离职、组织策略、集中更新、审计和
跨组织协作。ShareServer 提供一个可自建的组织控制面,让设备通过主动出站连接注册能力并
接收任务,不要求为每台桌面设备开放公网端口。
## 2. 产品目标
1. 组织可在自有网络、私有云或公网独立部署 ShareServer。
2. 管理员可统一管理用户、组、设备、能力、权限、更新和审计。
3. 成员通过桌面端使用获准能力,无需日常进入管理控制台。
4. 设备离线、用户撤销或策略变化能准确影响新任务,不破坏历史证据。
5. 两个组织可经双边确认建立有限范围的联邦协作。
6. ShareServer 可以只运行控制面,也可按部署策略提供数据中继和包存储。
## 3. 产品范围
### 3.1 包含
- 本地账号及可配置 OIDC 身份源。
- 组织、成员、角色、用户组、设备组和服务账号。
- 设备注册、状态、撤销和合规元数据。
- 能力目录、Publication、Grant、强制策略和审批。
- 设备出站控制通道、任务路由、取消和可选中继。
- 签名能力包仓库、发布通道、更新策略和撤销。
- ShareServer 间双边联邦。
- 审计、用量、保留和导出。
- Web 管理控制台和成员自助页面。
### 3.2 不包含
- 远程管理设备桌面、任意 Shell 或任意文件系统。
- 发现未注册的局域网设备。
- 保存或分发用户 API Key、Cookie 和桌面登录状态。
- 默认查看任务正文、附件和模型输出。
- 未经提供者发布自动盘点设备能力。
- 自动信任同域邮箱、同一网络或其他 ShareServer。
- 公共开放市场、计费结算或匿名能力调用。
- 替代 GoodBuddy 官方客户端、模型和 Agent 发布源。
## 4. 功能需求
### FR-1 实例初始化
- 首次启动通过一次性引导创建实例标识和首个系统管理员。
- 引导完成后永久关闭初始化入口,后续管理员通过正常身份流程创建。
- 实例公开描述包含名称、协议版本、登录方式和联邦端点,不包含内部拓扑。
### FR-2 身份与组织
- 支持一个实例承载多个隔离组织,也支持部署为单组织模式。
- 支持本地账号和 OIDC;组织可限制允许的身份源。
- 成员可以属于多个组并拥有有界角色,角色不隐式跨组织。
- 停用用户立即停止签发新设备和任务凭据。
### FR-3 设备管理
- 桌面端经用户登录、一次性挑战和设备签名完成注册。
- 设备条目显示所有者、平台、客户端版本、最近在线、连接和撤销状态。
- 设备主动建立出站控制连接;服务端不能借此调用未发布能力。
- 管理员可撤销设备并终止其新任务资格。
### FR-4 能力目录
- 只接收已注册设备或服务节点提交的显式 Publication。
- 目录按调用者身份、设备状态、组织和策略过滤后返回。
- 支持同一能力多个版本和多个 provider,不掩盖来源。
- provider 离线时保留目录事实但显示不可用,不派发新任务。
### FR-5 授权策略
- Grant 可绑定用户、组、设备、设备组和联邦组织。
- 可约束能力、版本、动作、输入类型、数据分类、提供者、时间、次数、并发和委托。
- 支持组织强制策略和用户可收紧偏好;拒绝优先。
- 每个决定返回稳定结果、命中策略和面向用户的原因。
### FR-6 审批
- 调用、能力发布、权限扩大更新和联邦关系可以要求审批。
- 审批项显示申请人、对象、影响范围、数据类别、期限和策略依据。
- 批准结果有明确范围和有效期,不提供无边界“永久允许”。
- 申请内容变化后旧审批失效。
### FR-7 任务路由
- 根据冻结 provider、在线连接和路由策略把任务声明发送给目标设备。
- 支持直连协商、网关中继和取消传播。
- 任务正文与控制元数据分离;默认审计不保存正文。
- 已接受任务断线后允许查询同一 task,不自动创建重复执行。
### FR-8 能力包与更新
- 保存不可变签名 Package 和发布目录。
- 支持组织、组、平台、架构和维护窗口的更新策略。
- 支持固定版本、允许版本范围、暂停发布和紧急撤销。
- 权限扩大必须触发新批准,不能被普通自动更新策略覆盖。
- 包来源、大小、摘要、签名和下载结果可审计。
### FR-9 联邦
- 两个 ShareServer 通过指纹交换和双方管理员确认建立关系。
- 双方分别配置公开能力、允许主体、数据范围、用量和期限。
- 最终权限取双方策略交集;任一端撤销后停止新任务。
- 不默认链式转发到第三个组织。
### FR-10 审计与用量
- 记录身份、设备、发布、授权、审批、任务、包和联邦管理事件。
- 默认仅记录共享架构定义的有界元数据。
- 支持按组织隔离查询、条件筛选、分页和流式导出。
- 审计员只读,不能因查看日志取得任务正文或管理权限。
### FR-11 管理控制台
- 提供概览、组织与成员、设备、能力目录、权限策略、审批、发布与更新、网关联邦、任务与
中继、审计、系统设置。
- 普通成员只看到自己的设备、授权和待办;管理员栏目按权限隐藏并在 API 再次校验。
- 所有高影响操作说明对象、范围和后果。
### FR-12 可部署性
- 支持容器部署,配置与持久数据分离。
- 支持外部 PostgreSQL;对象存储和中继按功能启用。
- 提供健康、就绪、指标、结构化日志、备份和恢复说明。
- 服务升级不能要求同时升级所有在线桌面端;按协议版本协商兼容集合。
## 5. 产品验收
- 新实例可完成初始化、登录、创建组织、邀请成员和注册真实桌面设备。
- 两个不同组织不能通过 API、搜索、计数、错误或审计结果读取对方数据。
- 设备只有发布能力后才进入获准目录,撤销后不能接收新任务。
- 调用授权能同时处理用户、设备、组织、提供者、数据和联邦限制。
- 任务中继关闭时控制面仍可运行;中继开启时可取消并形成准确终态。
- 权限扩大的包更新不能自动部署。
- 两个独立 ShareServer 能建立、使用、暂停和撤销双边联邦。
- 备份恢复后组织、策略、发布和审计身份保持一致,短期连接和令牌不恢复。
@@ -1,242 +0,0 @@
# ShareServer 技术设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [ShareServer PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
| 共享协议 | [共享网络总体设计](../../architecture/share-network-architecture.md) |
## 1. 仓库与运行边界
实现时在仓库顶层创建独立项目:
```text
shareserver/
README.md
package.json
src/
api/
auth/
organizations/
devices/
capabilities/
policies/
approvals/
tasks/
relay/
packages/
federation/
audit/
web/
migrations/
deploy/
tests/
```
当前只提交设计文档,不创建无法运行的目录占位。服务端和 Desktop 共同依赖未来的
`packages/share-protocol`,ShareServer 不导入 Electron 的 `src/shared`。
## 2. 技术选型约束
- 使用与仓库一致的 TypeScript/Node.js 工具链,具体 HTTP 框架在实现前根据维护性选择。
- PostgreSQL 是组织、策略、任务元数据和审计的权威数据库。
- Package 与可选中继暂存使用 S3 兼容对象存储;不开启对应功能时不要求对象存储。
- Web 控制台与 API 同源部署,采用严格 CSP,不在浏览器保存设备凭据。
- 单实例部署可完整工作;水平扩展时再引入共享事件/连接路由组件,不为未验证规模预设复杂
分布式协议。
## 3. 服务模块
| 模块 | 职责 |
| --- | --- |
| `auth` | 本地身份、OIDC、Web session、服务和设备凭据 |
| `organizations` | 组织、成员、组、角色和范围解析 |
| `devices` | 注册挑战、设备记录、出站连接和在线状态 |
| `capabilities` | manifest、provider、Publication 和目录投影 |
| `policies` | Grant、强制策略和确定性授权计算 |
| `approvals` | 内容绑定申请、决定、过期和通知 |
| `tasks` | task/attempt、provider 路由、状态和取消 |
| `relay` | 可选有界数据中继,不解释能力正文 |
| `packages` | 不可变对象、签名目录、通道和撤销 |
| `federation` | 网关身份、offer、双边策略和跨组织映射 |
| `audit` | 事务内审计写入、查询、保留和导出 |
模块通过服务接口调用,API handler 不直接拼接跨组织 SQL 或自行计算权限。
## 4. API 边界
建议分为四类 audience:
```text
/api/v1/web/* 管理控制台与成员自助 API
/api/v1/device/* 设备注册、目录和任务控制 API
/api/v1/federation/* ShareServer 间 API
/api/v1/public/* 服务描述、健康与登录入口
```
设备使用 WebSocket `/api/v1/device/connect` 建立出站控制通道。包下载使用短期、绑定主体和对象
摘要的 URL。联邦 API 使用网关身份,不接受普通用户 session。每类 token 使用不同 issuer/
audience/scope,不能互换。
所有请求经 schema、认证、organization scope、授权、速率和大小边界。列表使用游标分页和固定
最大页;错误返回 request ID、稳定 code 和有界参数。
## 5. 数据模型
核心表建议为:
```text
instances
identity_providers
users
organizations
memberships
groups
group_members
role_bindings
registered_devices
device_credentials
device_connections
capabilities
capability_versions
providers
publications
grants
policies
approval_requests
approval_decisions
tasks
task_attempts
task_events
package_objects
package_releases
release_channels
update_policies
federation_trusts
federation_capability_exports
audit_events
revocations
```
要求:
- 所有组织数据表包含不可为空的 `organization_id`,并由 repository 层强制作用域。
- 唯一约束同时包含 organization,避免跨组织名称冲突泄漏。
- 密钥保存为单向摘要或经外部主密钥加密的密文;数据库不保存明文 token。
- 在线连接和短期 nonce 主要在内存;表中只保存恢复所需元数据,不把 socket 当持久对象。
- task event 只存控制状态和有界统计;正文流不进入普通关系表。
## 6. 事务与一致性
- 业务变更与对应审计事件在同一 PostgreSQL 事务提交。
- Publication/Grant/Policy 使用 revision 做乐观并发,Web 草稿不能覆盖他人更新。
- 授权决定创建 task 时再次读取当前 revision,并将命中策略摘要冻结到 task。
- 设备在线状态允许短暂最终一致;身份、撤销、授权和任务接受必须走权威存储。
- 重复注册挑战、联邦 offer 和 task create 使用唯一幂等键。
- 无法确认 provider 副作用时只更新 `outcome_unknown`,不靠消息队列自动重放。
## 7. 设备连接与任务
连接建立后完成设备 credential、organization、客户端协议和当前 registration 校验。服务端为
每个连接设置心跳、空闲、帧、发送队列和并发上限。
任务流程:
```text
authorize -> create task -> resolve exact provider connection
-> send declaration -> provider accepts/rejects
-> establish direct or relay route -> stream progress
-> provider terminal result -> caller delivery -> audit terminal state
```
同一设备重复连接时使用 connection generation;新连接接管未来任务,旧连接仍只允许完成或
查询已归属 task,随后有界关闭。不能把新任务随机发到身份相同但 generation 过期的 socket。
## 8. 中继
中继是可选模块:
- 控制面与中继可部署在不同进程,但共享 task 授权验证。
- 每个 relay ticket 绑定 task、调用方、provider、方向、期限和最大字节。
- 实现背压和全局/组织/设备并发及带宽上限。
- 不解析能力正文,不写普通日志;需要临时落盘时使用加密 GoodBuddy-owned 暂存并按 task 清理。
- 中继关闭、过载或策略禁止时返回准确路由错误,不静默转到其他节点。
## 9. Package 存储
上传先进入不可公开暂存:流式计算大小和 SHA-256,验证签名、manifest、版本、平台和权限后,
以 digest 发布为不可变对象。相同 digest 可去重,但组织授权和 Release 分别保存。
Release catalog 使用独立签名域。服务器不能重打包已签名 payload;镜像目标必须保持相同字节、
大小和 SHA-256。撤销更新目录和授权,不覆盖或原地删除仍受审计保留的对象。
## 10. 联邦协议
- 每个实例生成网关身份和可轮换证书,私钥不进入数据库备份明文。
- offer 包含双方可人工核对的组织名、端点、指纹、协议和 nonce。
- 双方确认后交换有界 capability projection 和本地映射 ID。
- 跨组织 task 带双方关联 ID 和短期签名声明;每端独立授权并各写审计。
- 不接受调用方提供的任意回调 URL、第三方 hop 或内部主体 ID。
- 证书轮换需要旧身份签名或双方重新确认;身份突变直接暂停关系。
## 11. Web 安全
- 使用 HttpOnly、Secure、SameSite cookie 和 CSRF 防护管理 Web session。
- OIDC 使用 state、nonce、PKCE 和固定回调地址。
- 富文本字段按纯文本显示;manifest/schema 使用安全代码查看器,不执行 HTML。
- 权限隐藏仅改善界面,所有 API 在服务端重新校验。
- 登录、设备注册、目录、审批、导出和联邦接口分别限速。
- 密钥字段只能替换、轮换和测试,永不回显。
## 12. 部署与配置
最小生产部署:
```text
TLS reverse proxy/load balancer
ShareServer API + Web
PostgreSQL
可选:S3 compatible object storage
可选:Relay worker
```
部署配置通过环境变量或挂载文件提供数据库、主密钥、公开 URL、OIDC、邮件、对象存储和限制。
普通 Web 管理员不能修改基础设施秘密。提供数据库迁移命令、不可变镜像、非 root 容器、健康
与就绪端点、优雅停止和部署示例。
## 13. 可观测性与备份
- 结构化日志只包含 request/task/organization 关联 ID、稳定事件和错误码。
- 指标覆盖请求、WebSocket、授权结果、任务状态、中继、Package 和联邦,不包含能力正文。
- tracing 跨内部服务传播关联 ID,跨联邦只传播双方同意的关联标识。
- 备份 PostgreSQL、Package 对象和网关身份;不备份缓存、Web session、nonce 或中继暂存。
- 恢复后撤销列表、组织 ID、签名目录和审计序列保持有效,所有设备重新连接。
## 14. 测试策略
### 14.1 自动化
- schema、认证 audience、CSRF、OIDC 和设备挑战。
- 每个 repository/API 的跨组织隔离与枚举防护。
- 拒绝优先授权、审批内容绑定、预算和并发。
- device generation、离线、撤销、重连和 provider 冻结。
- task 幂等、取消、结果未知和不自动重放。
- Package 流式校验、不可变性、权限 diff 和撤销。
- 联邦双方确认、策略交集、暂停、证书变化和禁止第三 hop。
- 审计事务、过滤、分页、导出和正文缺失。
### 14.2 集成与部署
- Desktop 与真实 ShareServer 完成注册、目录、调用、文件和取消。
- 两个隔离组织验证搜索、计数、错误、对象和审计不泄漏。
- 两个 ShareServer 完成联邦建立、跨组织调用和单方撤销。
- 中继关闭与开启、容量耗尽、连接中断和大文件背压。
- 数据库备份恢复、版本升级、滚动停止和设备重连。
- 生产 TLS、OIDC 和对象存储组合的端到端验证。
设计完成不代表服务已实现;只有独立部署和真实 Desktop 端到端路径通过后才能更新实施状态。
-159
View File
@@ -1,159 +0,0 @@
# ShareServer Web 管理界面设计
## 文档信息
| 项目 | 内容 |
| --- | --- |
| 状态 | 设计中 |
| 版本 | 0.1 |
| 日期 | 2026-09-03 |
| 关联 PRD | [ShareServer PRD](./prd.md) |
| 功能逻辑 | [功能逻辑设计](./logic-design.md) |
ShareServer 是独立 Web 产品,不直接复用 Electron 页面壳层实现,但沿用 GoodBuddy 的语义
词汇、状态含义和克制的运营工具风格。界面以列表、表格和主从详情为主,不使用营销首页或
装饰性卡片墙。
## 1. 使用对象
- 普通成员主要在 GoodBuddy Desktop 使用能力;Web 端只处理自己的设备、申请和授权。
- 组织管理员使用完整组织控制台。
- 系统管理员使用与组织控制台分开的实例管理范围。
- 审计员进入只读视图,不显示可变更控件。
登录后直接进入当前身份可用的实际控制台,不显示产品宣传页。
## 2. 全局框架
```text
顶部栏:ShareServer 实例名 | 当前组织 | 全局搜索 | 审批 | 账号
侧栏:
概览
组织与成员
设备
能力目录
权限策略
审批
发布与更新
网关联邦
任务与中继
审计
系统设置(仅系统管理员)
```
当前组织始终显示。切换组织会重新加载整个数据范围,不保留上一组织的选择、筛选或详情。
系统管理使用“实例范围”标识,不能伪装成某个组织。
## 3. 概览
概览只显示需要行动和运行事实:
- 待审批数量及最旧等待时间。
- 在线/离线/已撤销设备。
- 可用/离线/暂停能力。
- 当前任务、失败和结果未知。
- 包更新异常和紧急撤销。
- 联邦连接异常。
- 中继流量和容量,仅在中继启用时显示。
指标可点击进入已带筛选的对应页面。没有事件时不显示虚构趋势。
## 4. 组织与成员
使用“成员”“用户组”“角色”三个同级页签。成员表包含姓名、身份源、组、状态、设备数和
最近活动。邀请是唯一主操作;暂停和移除进入行末菜单。
成员详情采用抽屉,显示身份、组、角色、设备、有效 Grant、申请和审计。管理员不能查看成员
任务正文或桌面文件。
## 5. 设备
```text
设备 所有者 平台 状态 能力 最近在线
Jiang-PC Jiang Windows 在线 2 刚刚
GPU-Worker-02 AI Team Linux 离线 8 2 小时前
```
设备详情包含概览、连接、已发布能力、Grant 和活动。设备状态与 Publication 状态分别显示。
撤销设备使用危险确认,说明新任务、活动任务和重新注册影响。
## 6. 能力目录
目录以能力为第一列,提供者为可展开信息:
```text
能力 版本 提供者 可见范围 可用性
图像生成 2.1.0 3 个节点 设计组 2/3 在线
```
详情包含 manifest、provider、Publication、允许输入、数据政策、调用趋势和最近错误。管理员
看到的是组织元数据,不通过目录读取能力内部数据。
## 7. 权限策略
策略页分“强制策略”和“Grant”。创建和编辑使用结构化表单:主体、能力、动作、输入、提供者、
时间、预算、批准和委托。保存前提供自然语言摘要:
> 允许“设计组”从合规设备调用组织内“图像生成”2.x;只发送文本和图片;每人每日 20 次;
> 不允许下游委托。
策略冲突预览显示最终结果和命中规则,但不提供允许规则覆盖拒绝规则的顺序拖拽。
## 8. 审批
审批收件箱按调用、发布、更新和联邦分类。详情必须显示申请人、能力或对象、目标范围、数据
类别、权限变化、期限和策略依据。批准按钮使用具体文案,例如“允许本次调用”“批准此版本
发布”,不能只有“同意”。
批量批准仅用于同类型、同策略且不包含权限扩大的低影响申请;联邦和权限扩大更新不支持
批量批准。
## 9. 发布与更新
页面分“能力包”“发布通道”“更新策略”“撤销记录”。Package 表显示版本、发布者、签名、
大小、目标平台、权限变化和状态。上传后先进入验证,不直接发布。
版本详情提供 manifest、摘要、签名、兼容范围、权限 diff、依赖和下载事实。紧急撤销位于
独立 danger zone,需要输入版本标识确认,并说明受影响设备和新任务。
## 10. 网关联邦
```text
合作组织 对端地址 状态 开放能力 有效期
法务服务商 share.legal.example 已连接 2 项 12-31
```
建立关系采用向导:填写端点、核对组织和指纹、选择公开范围、设置调用与数据限制、生成 offer、
等待对方确认、复核双方交集。只有双方完成后显示“已连接”。
详情明确展示“我方开放”“对方开放”“最终可用”三个集合。暂停和撤销说明对活动任务的处理。
## 11. 任务与中继
任务表显示调用方、能力、provider、路由、数据类别、状态、耗时和时间。默认不显示正文。
活动任务提供取消;结果未知使用独立状态并显示 provider 最后确认点。
中继未启用时页面显示控制面正常和启用位置,不展示零值图表。启用后显示活动连接、吞吐、
排队、容量和拒绝原因,不提供查看正文的调试入口。
## 12. 审计
提供操作者、动作、对象、组织、结果、时间和关联 ID 的可筛选表格。导出前显示条件、估算行数、
时区和字段范围;大导出作为后台任务生成有时效下载。审计详情保持只读,不把原始内部错误、
认证信息或任务正文显示给管理员。
## 13. 系统设置
只包含实例名称、公开地址、身份源、邮件、数据库/对象存储状态、中继开关与上限、保留策略、
联邦总开关、证书和版本。秘密字段只支持替换或测试,不回显原值。
运行时基础设施配置优先来自部署环境;Web 界面不能修改数据库连接串、主密钥或容器挂载路径。
## 14. 状态、响应式与无障碍
- 加载、无数据、无匹配、无权限、服务错误和部分不可用分别展示。
- 管理表格支持键盘访问、可见焦点、表头关联和分页;行菜单有可访问名称。
- 窄屏将表格转为字段列表,保持组织、来源、状态和风险信息,不把管理动作挤成不可辨图标。
- 所有时间显示实例时区并可查看 ISO 时间;联邦事件同时显示对端时间时标明时区。
- 状态不只依赖颜色。危险操作在确认后保持原页面上下文和可恢复错误。
+19 -19
View File
@@ -4,9 +4,9 @@
| 项目 | 内容 |
| --- | --- |
| 状态 | 入口与范围已实现;未来分区记忆待独立设计 |
| 版本 | 0.5 |
| 日期 | 2026-08-19 |
| 状态 | 入口与范围已实现;分区记忆协同待设计 |
| 版本 | 0.6 |
| 日期 | 2026-09-05 |
| 适用产品 | GoodBuddy 桌面端 |
| 文档角色 | 智能心跳当前能力、权威入口、范围和长期边界 |
| 相关设计 | [统一界面设计系统](../../../UI-DESIGN.md) |
@@ -14,8 +14,8 @@
| 相关界面 | [通用助手工作栏与执行空间 PRD](../assistant-workbar/prd.md) |
| Task 模型 | [Task 与 Job 统一领域模型](../task-and-job/task-and-job-model.md) |
> 本文区分近期实施与长期方向。未完成设计的“未来分区记忆”不得转写为数据结构、状态机、
> 页面、迁移或验收条件。
> 本文区分近期实施与长期方向。分区记忆由[分区记忆 PRD](../memory/prd.md)定义;智能心跳
> 不能另行创建平行的记忆类型、表或页面。
## 1. 已确认的产品边界
@@ -26,23 +26,23 @@
- 智能心跳不并入通用自动化中心,也不作为任务中心条目。
- 现有心跳报告、记忆建议、行动建议和审计历史在本轮继续保留。
### 1.2 长期方向:未来分区记忆
### 1.2 长期方向:分区记忆协同
已确认的方向只有:
> 智能心跳未来应成为按范围隔离的未来记忆。
> 智能心跳后续应按分区回顾本地信息,提出对应分区的记忆候选和可选唤起建议。
以下内容尚未设计:
- “未来记忆”的数据结构与存储方式。
- 与当前长期记忆、心跳报告和建议的关系。
- 唤起条件、状态和生命周期。
- 新增 Automation、Run 和 Agent 等分区后的逐分区回顾规则。
- 与当前记忆候选、心跳报告和建议的关系。
- 时间与内部事件唤起条件的字段、状态和生命周期。
- 到期后的页面和交互。
- 是否以及如何创建 Task。
- 旧版心跳数据如何迁移。
本轮不得实现 `FutureMemory`、新增未来记忆数据库表,或制作“未来记忆 / 即将唤起 /
已唤起”等页面。后续需要独立 PRD 和用户确认。
本轮不得扩展分区记忆类型或表。后续唤起能力必须作为分区记忆条目的可选条件,并继续遵守
该条目的分区读取边界。
## 2. 本轮实施范围
@@ -56,7 +56,7 @@
### 2.2 非目标
- 不设计或实现未来分区记忆。
- 不扩展分区记忆模型或实现唤起条件。
- 不修改 Task、Job 或 Task Center 模型。
- 不新增独立 Automation Center。
- 不移除定时任务现有入口。
@@ -181,7 +181,7 @@
## 6. 数据与安全要求
目标合同只扩展现有心跳范围,不引入 Future Memory:
目标合同只扩展现有心跳范围,不扩展分区记忆合同:
```ts
type HeartbeatScope =
@@ -217,11 +217,11 @@ type HeartbeatScope =
- 如需要,设置中心仅保留“打开智能心跳”导航。
- 验证应用内仍只有一个完整配置入口。
### 后续:独立设计
### 后续:分区记忆协同设计
- 未来分区记忆。
- 与长期记忆的关系。
- 唤起模型与生命周期。
- 按 Global、Project、Automation、Run 和 Agent 分区独立回顾并提出候选。
- 与已确认记忆、候选、心跳报告和建议的关系。
- 时间与内部事件唤起条件及生命周期。
- 与 Task 的关系。
这些内容需要新的 PRD 和明确确认,不属于本轮实现。
@@ -242,5 +242,5 @@ type HeartbeatScope =
- [x] 设置中心不再包含单条心跳 CRUD。
- [x] 任务中心本身以及定时任务现有行为没有被删除。
- [x] 智能心跳仍保持只读、禁用工具和有界输入输出。
- [x] 没有新增 Future Memory 表、状态机或未确认页面。
- [x] 没有新增平行的记忆类型、表或页面。
- [x] 加载失败、空状态、字段错误和危险操作满足统一设计与无障碍要求。
+6 -6
View File
@@ -52,7 +52,7 @@ GoodBuddy 应能够:
Task 时不保留空的 Task 上下文栏。
- 右侧栏在对话、知识库和活动视图之间保持状态。
- 知识图谱实体详情复用同一右栏容器,不再维护独立布局。
- 八个标准能力固定可达;能力、连接和数据状态可以变化,但应用不按当前上下文自动隐藏入口。
- 九个标准能力固定可达;能力、连接和数据状态可以变化,但应用不按当前上下文自动隐藏入口。
- Task Center 是 Task 的单例索引;其他可绑定目标的能力支持跟随当前上下文或固定到用户选择的目标。
### 3.2 右侧工作栏
@@ -169,8 +169,8 @@ GoodBuddy 应能够:
- 当前智能心跳继续提供周期回顾、报告、记忆建议、行动建议和运行历史。
- “智能心跳”菜单入口负责完整配置,并支持 Global 或指定一个、多个 Project。
- 任务中心和设置中心不复制智能心跳 CRUD。
- 智能心跳长期方向是“未来分区记忆”,但数据结构、唤起模型、生命周期、页面以及与任务和
长期记忆的关系尚未设计,不能提前实现。
- 智能心跳长期按分区回顾信息并提出记忆候选或唤起建议,但协同规则、唤起条件、生命周期、
页面以及与任务的关系尚未设计,不能提前实现平行的记忆类型或页面。
### 4.6 成果和预览
@@ -327,9 +327,9 @@ GoodBuddy 应能够:
### 8.1 右侧栏
- 三种窗口宽度下布局可用。
- 八个标准能力在主要视图中固定可达,应用不按能力自动隐藏。
- 九个标准能力在主要视图中固定可达,应用不按能力自动隐藏。
- 跨主视图切换保持栏目、折叠、跟随和固定目标状态。
- 任务中心、监督、Runtime、终端、进程、工作区、浏览器和成果更新不要求离开当前主任务。
- 任务中心、监督、Runtime、终端、进程、工作区、浏览器、资源监控和成果更新不要求离开当前主任务。
- 键盘可操作,并具备正确 ARIA 标签。
### 8.2 Projects
@@ -355,7 +355,7 @@ GoodBuddy 应能够:
- 用户删除后不再检索到。
- 每条记忆显示来源与作用域。
- 智能心跳配置明确属于 Global 或指定 Project,当前报告和建议继续沿用已有生命周期。
- 未来分区记忆完成独立设计前,不新增相关数据、页面或任务转换。
- 分区记忆协同和唤起条件完成设计前,不新增相关数据、页面或任务转换。
### 8.5 安全
+1
View File
@@ -90,6 +90,7 @@ export default defineConfig({
'@deepseek-ai/dsh-sandbox',
'@deepseek-ai/dsh-sandbox-policy',
'@deepseek-ai/dsh-session',
'@deepseek-ai/dsh-session-projection',
'@deepseek-ai/dsh-shell-env',
'@deepseek-ai/dsh-skill',
'@deepseek-ai/dsh-subprocess-local',
+9 -1
View File
@@ -12,11 +12,19 @@ export default tseslint.config(
'coverage/**',
'dist/**',
'node_modules/**',
'out/**'
'out/**',
'shareserver/**'
]
},
js.configs.recommended,
...tseslint.configs.recommended,
{
languageOptions: {
parserOptions: {
tsconfigRootDir: import.meta.dirname
}
}
},
{
files: ['src/main/**/*.ts', 'src/preload/**/*.ts', 'src/shared/**/*.ts'],
languageOptions: {
+972 -666
View File
File diff suppressed because it is too large Load Diff
+37 -28
View File
@@ -1,6 +1,6 @@
{
"name": "goodbuddy",
"version": "0.12.4",
"version": "0.12.8",
"private": true,
"description": "Secure desktop AI workspace with controlled Agent Runtimes",
"desktopName": "GoodBuddy",
@@ -127,6 +127,13 @@
"opencode.exe"
]
},
{
"from": ".runtime-resources",
"to": "runtimes",
"filter": [
"opencode-config{,/**/*}"
]
},
{
"from": "node_modules/opencode-ai/LICENSE",
"to": "licenses/opencode-ai-LICENSE"
@@ -263,33 +270,35 @@
},
"dependencies": {
"@agentclientprotocol/sdk": "0.25.1",
"@deepseek-ai/cordis": "4.0.1",
"@deepseek-ai/dsh-agent": "0.1.0-rc.8",
"@deepseek-ai/dsh-agent-loop": "0.1.0-rc.8",
"@deepseek-ai/dsh-bash-local": "0.1.0-rc.8",
"@deepseek-ai/dsh-credentials": "0.1.0-rc.8",
"@deepseek-ai/dsh-fs-local": "0.1.0-rc.8",
"@deepseek-ai/dsh-llm": "0.1.0-rc.8",
"@deepseek-ai/dsh-llm-pi-ai": "0.1.0-rc.8",
"@deepseek-ai/dsh-pwsh-local": "0.1.0-rc.8",
"@deepseek-ai/dsh-sandbox": "0.1.0-rc.8",
"@deepseek-ai/dsh-sandbox-policy": "0.1.0-rc.8",
"@deepseek-ai/dsh-session": "0.1.0-rc.8",
"@deepseek-ai/dsh-shell-env": "0.1.0-rc.8",
"@deepseek-ai/dsh-skill": "0.1.0-rc.8",
"@deepseek-ai/dsh-subprocess-local": "0.1.0-rc.8",
"@deepseek-ai/dsh-system-prompt": "0.1.0-rc.8",
"@deepseek-ai/dsh-token-meter": "0.1.0-rc.8",
"@deepseek-ai/dsh-tool-bash": "0.1.0-rc.8",
"@deepseek-ai/dsh-tool-fs": "0.1.0-rc.8",
"@deepseek-ai/dsh-tool-pwsh": "0.1.0-rc.8",
"@deepseek-ai/dsh-tool-skill": "0.1.0-rc.8",
"@deepseek-ai/dsh-tools": "0.1.0-rc.8",
"@deepseek-ai/dsh-user-approval": "0.1.0-rc.8",
"@deepseek-ai/cordis": "4.0.2",
"@deepseek-ai/dsh-agent": "0.1.2-rc.1",
"@deepseek-ai/dsh-agent-loop": "0.1.2-rc.1",
"@deepseek-ai/dsh-bash-local": "0.1.2-rc.1",
"@deepseek-ai/dsh-credentials": "0.1.2-rc.1",
"@deepseek-ai/dsh-fs-local": "0.1.2-rc.1",
"@deepseek-ai/dsh-llm": "0.1.2-rc.1",
"@deepseek-ai/dsh-llm-pi-ai": "0.1.2-rc.1",
"@deepseek-ai/dsh-pwsh-local": "0.1.2-rc.1",
"@deepseek-ai/dsh-sandbox": "0.1.2-rc.1",
"@deepseek-ai/dsh-sandbox-policy": "0.1.2-rc.1",
"@deepseek-ai/dsh-session": "0.1.2-rc.1",
"@deepseek-ai/dsh-session-projection": "0.1.2-rc.1",
"@deepseek-ai/dsh-shell-env": "0.1.2-rc.1",
"@deepseek-ai/dsh-skill": "0.1.2-rc.1",
"@deepseek-ai/dsh-subprocess-local": "0.1.2-rc.1",
"@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
"@deepseek-ai/dsh-token-meter": "0.1.2-rc.1",
"@deepseek-ai/dsh-tool-bash": "0.1.2-rc.1",
"@deepseek-ai/dsh-tool-fs": "0.1.2-rc.1",
"@deepseek-ai/dsh-tool-pwsh": "0.1.2-rc.1",
"@deepseek-ai/dsh-tool-skill": "0.1.2-rc.1",
"@deepseek-ai/dsh-tools": "0.1.2-rc.1",
"@deepseek-ai/dsh-user-approval": "0.1.2-rc.1",
"@huggingface/tokenizers": "^0.1.3",
"@modelcontextprotocol/sdk": "^1.30.0",
"@napi-rs/canvas": "1.0.3",
"@opencode-ai/sdk": "^1.18.9",
"@opencode-ai/plugin": "1.18.29",
"@opencode-ai/sdk": "1.18.29",
"@wecom/aibot-node-sdk": "^1.0.6",
"@xterm/addon-fit": "0.11.0",
"@xterm/addon-search": "0.16.0",
@@ -350,7 +359,7 @@
"eslint-plugin-react-hooks": "^7.1.1",
"globals": "^17.8.0",
"jsdom": "^30.0.1",
"opencode-ai": "1.18.9",
"opencode-ai": "1.18.29",
"png-to-ico": "^3.0.2",
"pngjs": "^7.0.0",
"tar": "7.5.22",
@@ -368,7 +377,7 @@
"@koromix/koffi-win32-x64": "3.1.4"
},
"allowScripts": {
"@deepseek-ai/dsh-subprocess-local@0.1.0-rc.8": true,
"@deepseek-ai/dsh-subprocess-local@0.1.2-rc.1": true,
"@google/genai@1.52.0": true,
"cpu-features@0.0.10": true,
"electron-winstaller@5.4.0": true,
@@ -377,7 +386,7 @@
"fsevents@2.3.3": true,
"koffi@3.1.4": true,
"node-pty@1.2.0-beta.15": true,
"opencode-ai@1.18.9": true,
"opencode-ai@1.18.29": true,
"protobufjs@7.6.5": true,
"ssh2@1.17.0": true
}
+7 -3
View File
@@ -2,7 +2,7 @@
"formatVersion": 1,
"runtimes": {
"opencode": {
"version": "1.18.9",
"version": "1.18.29",
"provider": "opencode",
"entrypoint": "bin/opencode",
"entrypointIdentity": "opencode-acp",
@@ -27,11 +27,15 @@
"targets": {
"x64": {
"package": "opencode-linux-x64-baseline",
"integrity": "sha512-x4KiJk9EF7ktM18Ru5Jue4kTntxMvlhWb7tHniQGGRvY2KeoK1iIkyAFd7ri5H/fSkM22hNv/Gg1Jk6/h9IlxQ=="
"integrity": "sha512-AnPvoVjeLC6qBYSeDwmj8fIVeJxspkrh1xAPow5NMSdH7IZZd0auiI2PHIW+0/3B+xfar1Zx1a6yvDd4Wd74Wg=="
},
"darwin-arm64": {
"package": "opencode-darwin-arm64",
"integrity": "sha512-EU0qma5GPJXcDp5rENvedSfqJjqxatQW/Qzjs71pR2YdbrSh7YmBwtvnIb2/KIvfQr0DErX4ST14sbBQI2mQdg=="
},
"arm64": {
"package": "opencode-linux-arm64",
"integrity": "sha512-2IN4lLjhx2FICcMDnBsKgwrey0AvAM0SlNzzj7L71uakNxWvrhcqPYVpEhrEYUjIn+uQGMY5PjA+uupXigJE2A=="
"integrity": "sha512-Lr7XXik5wPJZBV5RW+aR6Uogf1n5GogpB565m+D/jiO/vRXr9LhvCpixgAr1tiwuH1ZfMsqOOlbFQz5jgH3Tqg=="
}
}
}
+156
View File
@@ -1,6 +1,162 @@
{
"formatVersion": 1,
"releases": [
{
"version": "0.12.8",
"releasedAt": "2026-09-08",
"notes": {
"zh-CN": {
"highlights": [],
"features": [],
"fixes": [
"**修复 OpenCode 启动失败。** 安装包补齐离线配置依赖,避免因缺少 `@opencode-ai/plugin/package.json` 无法启动。升级后无需手动安装 Node.js 或 npm 依赖。",
"**修复 DeepSeek Harness Host 启动退出。** 将必需的会话组件完整打入 Host,避免安装后提示“Host 启动前退出(code 1)”。现有模型配置保持不变。",
"**macOS 仅提供 DMG。** Intel 与 Apple 芯片版本不再构建或发布 ZIP。Windows 和 Linux 的安装包格式保持不变。"
],
"notices": [
"旧版 macOS 的 GitHub 更新检查,以及所有平台的旧版镜像更新检查可能无法识别新清单,请从官方下载页手动升级至 `0.12.8`。",
"Agent 继续使用 `0.11.21`,本次不重发 Agent,也不构建 LoongArch 实验预览。"
]
},
"en-US": {
"highlights": [],
"features": [],
"fixes": [
"**Fixed OpenCode startup failures.** Desktop packages now include the offline configuration dependencies, avoiding startup failures caused by a missing `@opencode-ai/plugin/package.json`. No manual Node.js or npm dependency installation is needed after upgrading.",
"**Fixed DeepSeek Harness Host startup exits.** Required session components are bundled into the Host, preventing the “Host exited before startup (code 1)” error after installation. Existing model settings remain unchanged.",
"**DMG-only macOS releases.** Intel and Apple silicon versions no longer build or publish ZIP packages. Windows and Linux package formats are unchanged."
],
"notices": [
"Older macOS GitHub update checks and older mirror update checks on all platforms may reject the new manifest. Upgrade manually to `0.12.8` from the official download page.",
"Agent remains at `0.11.21`; this release does not republish the Agent or include a LoongArch preview."
]
}
}
},
{
"version": "0.12.7",
"releasedAt": "2026-09-08",
"notes": {
"zh-CN": {
"highlights": [],
"features": [
"**参考图编辑。** 使用兼容的图像模型连接时,可以附加参考图片并描述修改要求。GoodBuddy 会发送图片编辑请求;服务商必须支持图片编辑接口。",
"**专家协作沿用工作模式。** 选择专家或团队分析时,专家可以按当前 Ask/Execute 模式使用已启用的本机直连模型工具。Ask 仍只读;这些工具不是远程 OpenCode 子会话。"
],
"fixes": [
"**Execute 不再卡在目录权限等待。** OpenCode、Continue 和 DeepSeek Harness 可以按当前账号权限访问工作区外目录。OpenCode 原生子代理的权限请求也会正确处理,Ask 保持只读。",
"**原生问答正确回传。** 本机 OpenCode 和 Continue 的选项、yes/no、自由文本及跳过通过现有问答卡片处理。修复 OpenCode 子会话提问丢失,以及 Continue 自定义答案回传错误。",
"**OpenCode 会话切换更稳定。** 切回会话时保留可用的原生操作栏,并在复用服务前检查其状态。并行请求的取消和临时 MCP 清理保持隔离。",
"**Runtime 启动更可靠。** OpenCode 使用随包提供并按版本复用的离线配置依赖,减少重复准备。修复 DeepSeek Harness 升级后的初始化失败。",
"**远程 Agent 安装与重连更可靠。** 修复包含 OpenCode 离线依赖的安装包被误判为元数据过大,以及部分合法握手被拒绝的问题。连接在握手期间关闭时会及时返回错误,不再一直等待。",
"**聊天输入与布局更顺畅。** 输入时不再重复渲染未变化的聊天历史,消息正文统一显示在头像和名称下方。终端移除多余内边距,让内容使用完整面板空间。"
],
"notices": [
"先升级至 Desktop `0.12.7`,再安装 Agent `0.11.21`,以获得远程修复。远程项目仍为技术预览;Agent 单独提供 Linux x64、Linux arm64 和 macOS arm64 包。",
"OpenCode 更新至 `1.18.29`;已运行的旧 Runtime 不会热更新,需要升级并重启。结构化业务问题仍需用户回答,不会自动代答终端密码、OAuth 或任意确认提示。",
"本次使用标准六平台桌面发布矩阵,不构建 LoongArch 实验预览。应用导航与系统工具入口文档仍为设计稿,不代表新界面已经交付。"
]
},
"en-US": {
"highlights": [],
"features": [
"**Reference-image editing.** Attach reference images and describe the changes when using a compatible image-model connection. GoodBuddy sends an image-edit request; the provider must support the image-editing endpoint.",
"**Experts inherit the work mode.** Selected experts and team analysis can use enabled local direct-model tools under the current Ask/Execute mode. Ask remains read-only; these tools are not remote OpenCode child sessions."
],
"fixes": [
"**Execute no longer stalls on directory permissions.** OpenCode, Continue, and DeepSeek Harness can access paths outside the workspace using the current account’s permissions. OpenCode native subagent permission requests are also handled correctly, while Ask stays read-only.",
"**Native questions return answers correctly.** Local OpenCode and Continue handle choices, yes/no, free text, and skipping through the existing question cards. This fixes lost OpenCode child-session questions and incorrect Continue custom-answer responses.",
"**More stable OpenCode session switching.** Returning to a conversation preserves usable native controls, and cached services are checked before reuse. Cancellation and temporary MCP cleanup remain isolated between parallel requests.",
"**More reliable Runtime startup.** OpenCode uses bundled offline configuration dependencies and reuses them by version, reducing repeated preparation. DeepSeek Harness initialization failures after upgrading are also fixed.",
"**More reliable remote Agent installation and reconnection.** Packages containing OpenCode’s offline dependencies are no longer incorrectly rejected as oversized metadata, and valid handshakes are no longer intermittently rejected. Connections closed during the handshake now return an error promptly instead of waiting indefinitely.",
"**Smoother typing and cleaner layouts.** Typing no longer rerenders unchanged chat history, and message content appears below the avatar and name. Removing extra terminal padding gives content the full panel area."
],
"notices": [
"Upgrade to Desktop `0.12.7` before installing Agent `0.11.21` for the remote fixes. Remote Projects remain a technical preview; separate Agent packages target Linux x64, Linux arm64, and macOS arm64.",
"OpenCode updates to `1.18.29`; running older Runtimes require an upgrade and restart rather than a live update. Structured business questions still require user answers; terminal passwords, OAuth, and arbitrary confirmation prompts are not answered automatically.",
"This release uses the standard six-target desktop matrix without a LoongArch preview. Application navigation and system-tool entry documents remain design drafts, not delivered UI."
]
}
}
},
{
"version": "0.12.6",
"releasedAt": "2026-09-06",
"notes": {
"zh-CN": {
"highlights": [],
"features": [],
"fixes": [
"**子代理过程与结果显示更完整。** 展开 OpenCode 子代理卡片,可以按顺序查看文字、推理和工具详情,最终结果单独展示。远程项目需要更新 Agent 才能查看实时过程;旧 Agent 仍可显示最终结果,但不会补造缺失的过程记录。",
"**工作区文件变更更容易查看。** 点击变更文件,可分别查看已暂存和未暂存差异,包括删除、重命名和未跟踪文件。刷新会更新当前差异和已展开目录,超过 50 项的变更可以继续加载。",
"**知识库支持整行勾选。** 点击知识库名称或所在行即可切换选择,不再因焦点变化导致点击无效。选择范围仍按当前对话保存。",
"**OpenCode 连续与并行对话更稳定。** 每次请求结束后会释放自身的事件连接,避免连续使用后连接不断积累。取消一条请求不会关闭其他并行会话的连接。",
"**命令可以使用工作区外目录。** 直连模型在 Execute 模式下,可以使用绝对路径、相对路径或符号链接指定工作目录。未指定时仍使用项目工作区,Ask 保持只读。",
"**恢复 GitHub 来源的 Agent 更新检查。** 选择 GitHub 后,可以正常读取可用 Agent 版本,不再因历史发布信息过大提示“发布目录超出大小限制”。镜像入口和已有本地包不受影响。"
],
"notices": [
"远程子代理实时过程和本轮远程文件差异改进需要更新至 Agent `0.11.20`。提供 Linux x64、Linux arm64 和 macOS arm64 包,Desktop 不内置 Agent。",
"本次使用标准六平台桌面发布矩阵,不构建 LoongArch 实验预览。"
]
},
"en-US": {
"highlights": [],
"features": [],
"fixes": [
"**More complete subagent progress and results.** Expand an OpenCode subagent card to view ordered text, reasoning, and tool details, with the final result shown separately. Remote projects require an updated Agent for live progress; older Agents still show final results without inventing missing progress history.",
"**Clearer workspace file changes.** Select a changed file to inspect staged and unstaged diffs separately, including deleted, renamed, and untracked files. Refresh updates the selected diff and expanded directories, and lists with more than 50 changes can load additional entries.",
"**Whole-row knowledge selection.** Click a knowledge library’s name or row to toggle its selection without focus changes losing the click. Selection remains scoped to the current conversation.",
"**More reliable consecutive and parallel OpenCode conversations.** Each request releases its own event connection when it ends, preventing connections from accumulating during continued use. Cancelling one request does not close other parallel conversations’ connections.",
"**Commands can use directories outside the workspace.** Direct models in Execute mode can select a working directory using an absolute path, a relative path, or a symbolic link. The project workspace remains the default, and Ask stays read-only.",
"**Restored Agent update checks through GitHub.** Selecting GitHub now retrieves available Agent versions without failing because the release history exceeds the response-size limit. Mirror access and existing local packages are unaffected."
],
"notices": [
"Update to Agent `0.11.20` for live remote subagent progress and the remote file-diff improvements in this release. Packages are available for Linux x64, Linux arm64, and macOS arm64; Desktop does not embed the Agent.",
"This release uses the standard six-target desktop matrix and does not include an experimental LoongArch preview."
]
}
}
},
{
"version": "0.12.5",
"releasedAt": "2026-09-06",
"notes": {
"zh-CN": {
"highlights": [],
"features": [
"**Apple Silicon Mac 远程项目。** 可以把 macOS arm64 设备添加为 SSH Host,安装独立 Agent 后使用 Ask、Execute 和断线重连。无需管理员权限或 launchd 服务;Intel Mac 暂不支持,远程项目仍为默认关闭的技术预览。"
],
"fixes": [
"**知识库使用流程更完整。** 创建和检索测试默认收起高级参数,资料可用后可直接“用于当前对话”;知识范围按对话保存,新对话默认不选择知识库。文档明确区分可用、处理中和失败,支持打开来源、失败重试,并在移除来源前说明影响和要求确认。",
"**切换窗口不再误中断回复。** 来回切换桌面窗口时,仍在生成的回复不会被误判为“上次运行意外中断”,后续消息也不会因此滞留队列。OpenCode 专属操作栏在会话刷新时保持稳定,减少界面抖动。",
"**长会话可以完整滚动。** 较长推理、工具记录或正文不再因估算高度导致滚动条提前到底。全部已加载内容可直接滚动查看,无需切换会话刷新。"
],
"notices": [
"**先升级桌面端,再更新 Agent。** Agent `0.11.19` 提供 Linux x64、Linux arm64 和 macOS arm64 包,共用一个签名目录。旧桌面端无法解析包含 macOS 条目的新目录,需升级至 Desktop `0.12.5` 后检查在线 Agent 更新;已有本地包和历史发布仍保留。",
"Desktop 不内置 Agent 或远程 Runtime。Ask 保持只读,Execute 使用所选 SSH 账号的完整权限。",
"本次使用标准六平台桌面发布矩阵,不构建 LoongArch 实验预览。",
"仓库新增独立 ShareServer 管理控制台交互原型,不包含在桌面安装包中,也不是已上线的团队共享服务;原型数据操作不持久化。"
]
},
"en-US": {
"highlights": [],
"features": [
"**Remote projects on Apple Silicon Macs.** Add a macOS arm64 device as an SSH Host and install the separate Agent to use Ask, Execute, and reconnection. No administrator access or launchd service is required; Intel Macs are not supported, and Remote Projects remain an off-by-default technical preview."
],
"fixes": [
"**A more complete knowledge workflow.** Creation and retrieval testing collapse advanced settings by default, and searchable libraries can be sent directly to the current conversation. Knowledge selection is saved per conversation and starts empty for new conversations; documents distinguish ready, processing, and failed states, with source opening, retry, and explicit removal confirmation.",
"**Window switching no longer interrupts active replies.** Returning to the desktop window no longer mislabels an active reply as interrupted or leaves subsequent messages queued because of that false state. OpenCode-specific controls remain stable during conversation refreshes, reducing layout shifts.",
"**Complete scrolling in long conversations.** Long reasoning, tool records, and replies no longer cause the scrollbar to end early because of estimated message heights. All loaded content remains reachable without switching conversations to refresh the layout."
],
"notices": [
"**Upgrade Desktop before updating the Agent.** Agent `0.11.19` provides Linux x64, Linux arm64, and macOS arm64 packages through one signed catalog. Older Desktop versions cannot parse the new catalog containing macOS entries; upgrade to Desktop `0.12.5` before checking online Agent updates. Existing local packages and historical releases remain available.",
"Desktop does not embed the Agent or Remote Runtime. Ask remains read-only; Execute uses the selected SSH account’s full permissions.",
"This release uses the standard six-target desktop matrix and does not include an experimental LoongArch preview.",
"The repository now includes a standalone interactive ShareServer console prototype. It is not bundled with Desktop or a deployed team-sharing service, and prototype data operations are not persisted."
]
}
}
},
{
"version": "0.12.4",
"releasedAt": "2026-09-05",
+2 -2
View File
@@ -81,7 +81,7 @@ https://github.com/mesalogo/goodbuddy/releases/latest
校验规则与桌面更新检查保持一致:索引只能指向稳定 SemVer 版本,必须恰好
包含 Windows、macOS、Linux 的 x64 / arm64 六个匹配目标;每个目标必须提供
准确的两种格式和扩展名、正的安全整数大小、64 字符小写十六进制 SHA-256、
准确的格式和扩展名(Windows 两种、macOS 仅 DMG、Linux 三种),正的安全整数大小、64 字符小写十六进制 SHA-256、
全局唯一文件名,以及位于
`https://goodbuddy.oss-cn-beijing.aliyuncs.com/releases/v${version}/`
下、对文件名进行 URL 编码的精确地址。校验清单和 GitHub 回退地址也必须
@@ -90,7 +90,7 @@ https://github.com/mesalogo/goodbuddy/releases/latest
安装包文件名必须与发布生成器完全一致:Windows 使用
`GoodBuddy-${version}-windows-${arch}-setup.exe` 与
`GoodBuddy-${version}-windows-${arch}-portable.zip`;macOS 使用
`GoodBuddy-${version}-mac-${arch}.dmg|zip`;Linux x64 的 AppImage、DEB 与
`GoodBuddy-${version}-mac-${arch}.dmg`;读取器仍接受历史 DMG/ZIP 清单,但当前页面不提供 macOS ZIP 选项。Linux x64 的 AppImage、DEB 与
RPM 分别使用 electron-builder 的 `x86_64`、`amd64` 与 `x86_64` 架构名。
Linux arm64 的 AppImage/DEB 使用 `arm64`,RPM 使用 `aarch64`。
Linux 处理器选择器另外提供龙芯 `loong64` 实验预览;选中后安装包类型固定为
+1 -1
View File
@@ -261,7 +261,7 @@
<path d="M16.8 12.7c0-2.7 2.2-4 2.3-4.1A5 5 0 0 0 15.2 6c-1.7-.2-3.2 1-4.1 1-.9 0-2.2-1-3.6-1-1.8 0-3.5 1.1-4.5 2.7-2 3.5-.5 8.7 1.4 11.5.9 1.4 2 2.8 3.5 2.7 1.4 0 1.9-.9 3.7-.9 1.7 0 2.2.9 3.7.9s2.5-1.4 3.4-2.7a10 10 0 0 0 1.6-3.3 4.6 4.6 0 0 1-3.5-4.2ZM14.1 4.3A4.7 4.7 0 0 0 15.2 1a4.8 4.8 0 0 0-3.1 1.6A4.4 4.4 0 0 0 11 5.8c1.2.1 2.3-.5 3.1-1.5Z" />
</svg>
</div>
<div><h3>macOS</h3><p>Apple silicon / Intel · DMG / ZIP</p></div>
<div><h3>macOS</h3><p>Apple silicon / Intel · DMG</p></div>
<a
class="button button--download"
href="https://github.com/mesalogo/goodbuddy/releases/latest"
+1 -2
View File
@@ -285,7 +285,7 @@
<path d="M16.8 12.7c0-2.7 2.2-4 2.3-4.1A5 5 0 0 0 15.2 6c-1.7-.2-3.2 1-4.1 1-.9 0-2.2-1-3.6-1-1.8 0-3.5 1.1-4.5 2.7-2 3.5-.5 8.7 1.4 11.5.9 1.4 2 2.8 3.5 2.7 1.4 0 1.9-.9 3.7-.9 1.7 0 2.2.9 3.7.9s2.5-1.4 3.4-2.7a10 10 0 0 0 1.6-3.3 4.6 4.6 0 0 1-3.5-4.2ZM14.1 4.3A4.7 4.7 0 0 0 15.2 1a4.8 4.8 0 0 0-3.1 1.6A4.4 4.4 0 0 0 11 5.8c1.2.1 2.3-.5 3.1-1.5Z" />
</svg>
</div>
<div><h3>macOS</h3><p>Apple 芯片 / Intel · DMG / ZIP</p></div>
<div><h3>macOS</h3><p>Apple 芯片 / Intel · DMG</p></div>
<div class="download-options" data-download-card="macos">
<label>
<span>处理器</span>
@@ -298,7 +298,6 @@
<span>类型</span>
<select data-download-format aria-label="macOS 安装包类型">
<option value="dmg">DMG(推荐)</option>
<option value="zip">ZIP</option>
</select>
</label>
</div>
+7 -4
View File
@@ -27,12 +27,12 @@
"macos-x64": Object.freeze({
platform: "macos",
arch: "x64",
formats: Object.freeze(["dmg", "zip"]),
formats: Object.freeze(["dmg"]),
}),
"macos-arm64": Object.freeze({
platform: "macos",
arch: "arm64",
formats: Object.freeze(["dmg", "zip"]),
formats: Object.freeze(["dmg"]),
}),
"linux-x64": Object.freeze({
platform: "linux",
@@ -143,6 +143,9 @@
for (const key of targetKeys) {
const definition = targetDefinitions[key];
const target = index.targets[key];
const formats = definition.platform === "macos" && target?.files?.zip
? ["dmg", "zip"]
: definition.formats;
assert(
hasExactKeys(target, ["platform", "arch", "files"]) &&
target.platform === definition.platform &&
@@ -150,11 +153,11 @@
`发布目标与键不匹配:${key}`,
);
assert(
hasExactKeys(target.files, definition.formats),
hasExactKeys(target.files, formats),
`发布目标文件格式无效:${key}`,
);
for (const format of definition.formats) {
for (const format of formats) {
const file = target.files[format];
assert(
hasExactKeys(file, ["name", "size", "sha256", "url"]),
+8
View File
@@ -128,6 +128,14 @@ test("accepts the canonical stable six-target release index", () => {
assert.equal(validateReleaseIndex(index), index);
});
test("accepts DMG-only releases with twelve installer entries", () => {
const index = validIndex();
delete index.targets["macos-x64"].files.zip;
delete index.targets["macos-arm64"].files.zip;
assert.equal(validateReleaseIndex(index), index);
assert.equal(Object.values(index.targets).flatMap((target) => Object.values(target.files)).length, 12);
});
test("rejects unstable or non-strict versions and extra top-level fields", () => {
for (const version of ["v1.2.3", "01.2.3", "1.2", "1.2.3-rc.1"]) {
expectRejected((index) => {
+1 -1
View File
@@ -538,7 +538,7 @@ const expectedDownloadOptions = {
},
macos: {
arches: ["arm64", "x64"],
formats: ["dmg", "zip"],
formats: ["dmg"],
},
linux: {
arches: ["x64", "arm64", "loong64"],
+6 -25
View File
@@ -1,6 +1,8 @@
import { createHash } from 'node:crypto'
import { createReadStream } from 'node:fs'
import { open } from 'node:fs/promises'
import { binaryTargetArchitecture } from '../shared/node/binary-target'
import type { AgentPlatform } from '../shared/agent-target'
import type {
AgentArchitecture
} from '../shared/agent-installation-contracts'
@@ -16,7 +18,8 @@ export async function sha256File(filePath: string): Promise<string> {
export async function assertElfArchitecture(
filePath: string,
expected: AgentArchitecture,
label: string
label: string,
platform: AgentPlatform = 'linux'
): Promise<void> {
const handle = await open(filePath, 'r')
try {
@@ -27,8 +30,8 @@ export async function assertElfArchitecture(
header.length,
0
)
const actual = detectElfArchitecture(
header.subarray(0, bytesRead)
const actual = binaryTargetArchitecture(
header.subarray(0, bytesRead), platform
)
if (actual !== expected) {
throw new Error(
@@ -39,25 +42,3 @@ export async function assertElfArchitecture(
await handle.close()
}
}
function detectElfArchitecture(
header: Buffer
): AgentArchitecture | undefined {
if (
header.length < 20 ||
header[0] !== 0x7f ||
header.toString('ascii', 1, 4) !== 'ELF' ||
(header[5] !== 1 && header[5] !== 2)
) {
return undefined
}
const machine =
header[5] === 2
? header.readUInt16BE(18)
: header.readUInt16LE(18)
return machine === 62
? 'x64'
: machine === 183
? 'arm64'
: undefined
}
+3 -3
View File
@@ -449,7 +449,7 @@ describe('Agent CLI fixed command contract', () => {
homeDirectory: 'C:\\Users\\tester',
uid: 1234
})
).toThrow('current Linux UID')
).toThrow('current Unix UID')
})
it('rejects injected installation IDs before lifecycle dispatch', async () => {
@@ -542,7 +542,7 @@ describe('Agent CLI fixed command contract', () => {
runtimeId: 'opencode',
bundleDigest,
architecture: 'x64',
runtimeVersion: '1.18.9'
runtimeVersion: '1.18.29'
})
expect(
@@ -704,7 +704,7 @@ function verifiedRuntime(
manifest: {
runtimeId: 'opencode',
provider: 'opencode',
runtimeVersion: '1.18.9',
runtimeVersion: '1.18.29',
architecture: 'x64',
signingKeyId: 'test-key',
bundleDigest,
+3 -3
View File
@@ -842,13 +842,13 @@ export function deriveManagedInstallationPaths(
const platform = options.platform ?? process.platform
const uid = options.uid ?? process.getuid?.()
if (
platform !== 'linux' ||
!['linux', 'darwin'].includes(platform) ||
uid === undefined ||
!Number.isSafeInteger(uid) ||
uid < 0
) {
throw new AgentUnsupportedError(
'Managed Agent paths require a current Linux UID',
'Managed Agent paths require a current Unix UID',
'platform-incompatible'
)
}
@@ -860,7 +860,7 @@ export function deriveManagedInstallationPaths(
.digest('hex')
.slice(0, 20)
const socketRoot = posix.join(
'/tmp',
platform === 'darwin' ? '/private/tmp' : '/tmp',
`goodbuddy-${uid}-${homeHash}`
)
const socketName = createHash('sha256')
+7 -3
View File
@@ -100,6 +100,7 @@ export class AgentDaemon {
readonly #agentVersion: string
readonly #protocol: AttachPreface['protocol']
readonly #architecture: 'x64' | 'arm64'
readonly #platform = process.platform
readonly #remoteUserIdentity: string
readonly #stateDirectory: string
readonly #socketPath: string
@@ -171,7 +172,7 @@ export class AgentDaemon {
daemonBootId: this.#bootId,
agentVersion: this.#agentVersion,
protocol: this.#protocol,
platform: 'linux',
platform: this.#platform,
architecture: this.#architecture,
supervisor: 'detached-on-demand',
remoteUserIdentity: this.#remoteUserIdentity,
@@ -180,9 +181,12 @@ export class AgentDaemon {
}
async start(): Promise<void> {
if (process.platform !== 'linux') {
if (
this.#platform !== 'linux' &&
!(this.#platform === 'darwin' && process.arch === 'arm64')
) {
throw new AgentUnsupportedError(
'GoodBuddy Agent daemon is certified only on Linux',
'GoodBuddy Agent daemon requires Linux or macOS arm64',
'platform-incompatible'
)
}
@@ -0,0 +1,57 @@
import { spawn } from 'node:child_process'
import { once } from 'node:events'
import { realpathSync } from 'node:fs'
import { describe, expect, it } from 'vitest'
import {
darwinBootId,
inspectDarwinProcess,
listDarwinProcessGroup,
stopDarwinDescendants
} from './darwin-process-identity'
describe('Darwin process identity', () => {
const native = process.platform === 'darwin' && process.arch === 'arm64' ? it : it.skip
native('reads actual process identity and enumerates a dedicated child group', async () => {
const current = inspectDarwinProcess(process.pid)
expect(current?.pid).toBe(process.pid)
expect(current?.executablePath).toBe(realpathSync(process.execPath))
expect(BigInt(current!.starttime)).toBeGreaterThan(0n)
expect(darwinBootId()).toMatch(/^[a-f0-9-]{36}$/u)
const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], {
detached: true, stdio: 'ignore'
})
await once(child, 'spawn')
try {
const identity = inspectDarwinProcess(child.pid!)
expect(identity?.processGroupId).toBe(child.pid)
expect(listDarwinProcessGroup(child.pid!)).toContain(child.pid)
expect(listDarwinProcessGroup(child.pid!)).not.toContain(process.pid)
} finally {
const exited = once(child, 'exit')
child.kill()
await exited
}
expect(inspectDarwinProcess(child.pid!)).toBeUndefined()
})
native('stops a tool child that starts its own process group', async () => {
const parent = spawn(process.execPath, ['-e', `
const {spawn}=require('node:child_process');
const child=spawn(process.execPath,['-e','setInterval(()=>{},1000)'],{detached:true,stdio:'ignore'});
child.on('spawn',()=>console.log(child.pid));
setInterval(()=>{},1000);
`], { detached: true, stdio: ['ignore', 'pipe', 'ignore'] })
const [output] = await once(parent.stdout!, 'data')
const childPid = Number(String(output).trim())
try {
expect(inspectDarwinProcess(childPid)?.processGroupId).toBe(childPid)
await stopDarwinDescendants(parent.pid!, Date.now() + 3000)
expect(inspectDarwinProcess(childPid)).toBeUndefined()
expect(inspectDarwinProcess(parent.pid!)).toBeDefined()
} finally {
await stopDarwinDescendants(parent.pid!, Date.now() + 3000)
const exited = once(parent, 'exit')
parent.kill()
await exited
}
})
})
+139
View File
@@ -0,0 +1,139 @@
import koffi from 'koffi'
import { realpathSync } from 'node:fs'
export type DarwinProcessIdentity = {
pid: number
starttime: string
executablePath: string
processGroupId: number
parentPid: number
}
let binding: ReturnType<typeof createBinding> | undefined
function createBinding() {
const library = koffi.load('/usr/lib/libSystem.B.dylib')
return {
info: library.func('proc_pidinfo', 'int', [
'int', 'int', 'uint64_t', 'void *', 'int'
]),
path: library.func('proc_pidpath', 'int', ['int', 'void *', 'uint32_t']),
list: library.func('proc_listpids', 'int', ['uint32_t', 'uint32_t', 'void *', 'int']),
sysctl: library.func('sysctlbyname', 'int', [
'str', 'void *', 'void *', 'void *', 'size_t'
])
}
}
function native() {
if (process.platform !== 'darwin' || process.arch !== 'arm64') {
throw new Error('Darwin process inspection requires macOS arm64')
}
return binding ??= createBinding()
}
export function inspectDarwinProcess(pid: number): DarwinProcessIdentity | undefined {
if (!Number.isSafeInteger(pid) || pid <= 0) throw new Error('Invalid process ID')
const api = native()
const info = Buffer.alloc(136)
const size = api.info(pid, 3, 0, info, info.length) as number
if (size === 0 && [3, 2].includes(koffi.errno())) return undefined
if (size !== info.length || info.readUInt32LE(12) !== pid) {
throw new Error(`proc_pidinfo failed (${koffi.errno()})`)
}
const path = Buffer.alloc(4096)
if ((api.path(pid, path, path.length) as number) <= 0) {
if ([3, 2].includes(koffi.errno())) return undefined
throw new Error(`proc_pidpath failed (${koffi.errno()})`)
}
const starttime = info.readBigUInt64LE(120) * 1_000_000n +
info.readBigUInt64LE(128)
return {
pid,
starttime: starttime.toString(),
executablePath: realpathSync(path.subarray(0, path.indexOf(0)).toString('utf8')),
processGroupId: info.readUInt32LE(100),
parentPid: info.readUInt32LE(16)
}
}
export function darwinBootId(): string {
const bytes = Buffer.alloc(128)
const size = Buffer.alloc(8)
size.writeBigUInt64LE(BigInt(bytes.length))
if (native().sysctl('kern.bootsessionuuid', bytes, size, null, 0) !== 0) {
throw new Error(`Boot session lookup failed (${koffi.errno()})`)
}
const value = bytes.subarray(0, bytes.indexOf(0)).toString('ascii').toLowerCase()
if (!/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/u.test(value)) {
throw new Error('Invalid Darwin boot session identity')
}
return value
}
export function listDarwinProcessGroup(groupId: number): number[] {
return listDarwinPids(2, groupId)
}
function listDarwinPids(kind: number, parent: number): number[] {
const api = native()
if (!Number.isSafeInteger(parent) || parent <= 0) throw new Error('Invalid process group')
const required = api.list(kind, parent, null, 0) as number
if (required < 0) throw new Error('Process group enumeration failed')
const buffer = Buffer.alloc(required + 4096)
const size = api.list(kind, parent, buffer, buffer.length) as number
if (size < 0 || size >= buffer.length || size % 4 !== 0) {
throw new Error('Process group enumeration was incomplete')
}
const pids: number[] = []
for (let offset = 0; offset < size; offset += 4) {
const pid = buffer.readInt32LE(offset)
if (pid > 0) pids.push(pid)
}
return pids
}
export async function stopDarwinDescendants(
ownerPid: number,
deadline: number
): Promise<void> {
const descendants: DarwinProcessIdentity[] = []
const pending = [ownerPid]
const seen = new Set(pending)
while (pending.length > 0) {
const parentPid = pending.pop()!
for (const pid of listDarwinPids(6, parentPid)) {
if (seen.has(pid)) continue
seen.add(pid)
const identity = inspectDarwinProcess(pid)
if (!identity || identity.parentPid !== parentPid) continue
descendants.push(identity)
pending.push(pid)
}
}
const matches = (identity: DarwinProcessIdentity): boolean => {
const current = inspectDarwinProcess(identity.pid)
return current !== undefined &&
current.starttime === identity.starttime &&
current.executablePath === identity.executablePath
}
const signal = (identity: DarwinProcessIdentity, value: NodeJS.Signals): void => {
if (!matches(identity)) return
try { process.kill(identity.pid, value) }
catch (error) {
if (!(error instanceof Error && 'code' in error && error.code === 'ESRCH')) throw error
}
}
for (const identity of [...descendants].reverse()) signal(identity, 'SIGTERM')
const grace = Math.min(deadline, Date.now() + 500)
while (Date.now() < grace && descendants.some(matches)) {
await new Promise(resolve => setTimeout(resolve, 25))
}
for (const identity of [...descendants].reverse()) signal(identity, 'SIGKILL')
while (Date.now() < deadline && descendants.some(matches)) {
await new Promise(resolve => setTimeout(resolve, 25))
}
if (descendants.some(matches)) {
throw new Error('Runtime child process cleanup could not be verified')
}
}
@@ -384,7 +384,7 @@ function manifest(): RemoteRuntimeBundleManifest {
formatVersion: 2,
product: 'GoodBuddy',
runtimeId: 'opencode',
runtimeVersion: '1.18.9',
runtimeVersion: '1.18.29',
provider: 'opencode',
platform: 'linux',
architecture: 'x64',
@@ -409,7 +409,7 @@ function manifest(): RemoteRuntimeBundleManifest {
licenses: [
{
package: 'opencode-ai',
version: '1.18.9',
version: '1.18.29',
spdx: 'MIT',
path: 'licenses/opencode.txt'
}
@@ -28,6 +28,7 @@ import {
sameLinuxRuntimeProcessIdentity,
type LinuxRuntimeProcessIdentity
} from './runtime-process-identity'
import { listDarwinProcessGroup, stopDarwinDescendants } from './darwin-process-identity'
const DEFAULT_MAXIMUM_STDIN_WRITE_BYTES = 1024 * 1024
const DEFAULT_MAXIMUM_OUTPUT_QUEUE_CHUNKS = 128
@@ -401,6 +402,9 @@ export class DirectLinuxStdioProcessOwner {
throw identityError('Runtime PID identity changed before signal')
}
this.#registry.markStopping(this.ownerId)
if (process.platform === 'darwin') {
await stopDarwinDescendants(this.processIdentity.pid, Date.parse(deadlineAt))
}
this.#stdinClosed = true
this.#child.stdin.end()
this.#sendSignal(-record.processIdentity.processGroupId, 'SIGTERM')
@@ -540,9 +544,9 @@ export class DirectLinuxStdioProcessOwner {
export async function launchDirectLinuxStdioProcessOwner(
options: DirectLinuxStdioProcessOwnerOptions
): Promise<DirectLinuxStdioProcessOwner> {
if ((options.platform ?? process.platform) !== 'linux') {
if (!['linux', 'darwin'].includes(options.platform ?? process.platform)) {
throw new DirectLinuxStdioProcessOwnerError(
'Direct Runtime ownership is Linux-only',
'Direct Runtime ownership requires Linux or macOS',
'platform'
)
}
@@ -786,6 +790,18 @@ export async function listLinuxPidNamespaceMembers(
>,
options: { procRoot?: string } = {}
): Promise<LinuxRuntimeProcessIdentity[]> {
if (process.platform === 'darwin') {
const identities: LinuxRuntimeProcessIdentity[] = []
for (const pid of listDarwinProcessGroup(owner.processGroupId)) {
try {
const identity = await readLinuxRuntimeProcessIdentity(pid, { bootId: owner.bootId })
if (identity.processGroupId === owner.processGroupId) identities.push(identity)
} catch (error) {
if (!isMissingProcess(error)) throw error
}
}
return identities.sort((left, right) => left.pid - right.pid)
}
const procRoot = options.procRoot ?? '/proc'
const entries = await readdir(procRoot, { withFileTypes: true })
const identities: LinuxRuntimeProcessIdentity[] = []
@@ -230,7 +230,8 @@ export async function verifyPublishedAgentBundle(
await assertElfArchitecture(
join(installationDirectory, manifest.entrypoint.runtimePath),
manifest.arch,
'Agent Node runtime'
'Agent Node runtime',
manifest.platform
)
const manifestSha256 = sha256Bytes(manifestBytes)
return {
@@ -7,6 +7,7 @@ import {
import { isAbsolute, join, resolve } from 'node:path'
import { z } from 'zod'
import { assertAbsoluteManagedPath } from './managed-paths'
import { inspectDarwinProcess } from './darwin-process-identity'
const linuxProcessIdentitySchema = z
.object({
@@ -45,6 +46,14 @@ export class LinuxProcessInspector {
inspect(pidInput: number): LinuxProcessIdentity | undefined {
const pid = positivePid(pidInput)
if (process.platform === 'darwin') {
const identity = inspectDarwinProcess(pid)
return identity === undefined ? undefined : {
pid: identity.pid,
starttime: identity.starttime,
executablePath: identity.executablePath
}
}
try {
const starttime = parseStarttime(
this.#fileSystem.readText(
+3 -3
View File
@@ -287,20 +287,20 @@ export function derivePrivateTemporaryRoot(options: {
const platform = options.platform ?? process.platform
const uid = options.uid ?? process.getuid?.()
if (
platform !== 'linux' ||
!['linux', 'darwin'].includes(platform) ||
uid === undefined ||
!Number.isSafeInteger(uid) ||
uid < 0
) {
throw new ManagedPathError(
'Private Agent endpoint root requires a current Linux UID'
'Private Agent endpoint root requires a current Unix UID'
)
}
const canonicalHomeDirectory = resolve(
options.homeDirectory ?? homedir()
)
const temporaryDirectory = resolve(
options.temporaryDirectory ?? '/tmp'
options.temporaryDirectory ?? (platform === 'darwin' ? '/private/tmp' : '/tmp')
)
assertAbsoluteManagedPath(canonicalHomeDirectory)
assertAbsoluteManagedPath(temporaryDirectory)
+43 -17
View File
@@ -1,5 +1,9 @@
import { randomBytes, randomUUID } from 'node:crypto'
import { spawn as nodeSpawn } from 'node:child_process'
import { mkdtemp, writeFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { pathToFileURL } from 'node:url'
import { openCodeSubagentPluginSource } from './opencode-subagent-plugin'
import {
createServer,
type IncomingHttpHeaders,
@@ -8,7 +12,7 @@ import {
type ServerResponse
} from 'node:http'
import type { Socket } from 'node:net'
import { isAbsolute, resolve } from 'node:path'
import { isAbsolute, resolve, join } from 'node:path'
import type {
ModelBridgeModelProtocol
} from '../shared/model-bridge-contracts'
@@ -44,13 +48,13 @@ export const MODEL_BRIDGE_SDK_AUTH_SENTINEL =
export type OpenCodeModelBridgeProviderConfig = {
model: string
permission?: 'ask'
permission: 'ask' | 'allow'
agent: {
title: {
disable: true
}
build?: {
permission: 'ask'
build: {
permission: 'ask' | 'allow'
}
}
provider: Record<
@@ -409,7 +413,11 @@ export async function runOpenCodeModelBridgeHelper(options: {
const exchange = createUnixModelBridgeExchange({ socketPath })
const proxy = new ModelBridgeLoopbackProxy({ exchange })
const origin = await proxy.listen()
let pluginDirectory: string | undefined
try {
pluginDirectory = await mkdtemp(join(tmpdir(), 'goodbuddy-opencode-plugin-'))
const pluginPath = join(pluginDirectory, 'subagent.mjs')
await writeFile(pluginPath, openCodeSubagentPluginSource(), 'utf8')
const config = createOpenCodeModelBridgeProviderConfig({
protocol: options.protocol,
model: options.model,
@@ -419,7 +427,10 @@ export async function runOpenCodeModelBridgeHelper(options: {
})
const environment = credentialFreeHelperEnvironment(
options.environment ?? process.env,
JSON.stringify(config)
JSON.stringify({
...config,
plugin: [pathToFileURL(pluginPath).href]
})
)
const child = (options.spawn ?? defaultHelperSpawn)(
opencodeEntrypoint,
@@ -442,6 +453,9 @@ export async function runOpenCodeModelBridgeHelper(options: {
})
} finally {
await proxy.close()
if (pluginDirectory) {
await rm(pluginDirectory, { recursive: true, force: true })
}
}
}
@@ -507,11 +521,7 @@ export function createOpenCodeModelBridgeProviderConfig(input: {
const baseURL = `${origin}/v1`
return {
model: openCodeModelBridgeModelId(input.protocol, model),
...(input.workMode === 'ask'
? {
permission: 'ask' as const
}
: {}),
permission: input.workMode === 'ask' ? 'ask' : 'allow',
// GoodBuddy owns conversation titles. A first-prompt title request can
// race the real ACP prompt, abandon its Provider response, and correctly
// poison the no-replay ledger before the user request can run.
@@ -519,13 +529,9 @@ export function createOpenCodeModelBridgeProviderConfig(input: {
title: {
disable: true
},
...(input.workMode === 'ask'
? {
build: {
permission: 'ask' as const
}
}
: {})
build: {
permission: input.workMode === 'ask' ? 'ask' : 'allow'
}
},
provider: {
[providerId]: {
@@ -876,6 +882,26 @@ function credentialFreeHelperEnvironment(
}
environment[name] = value
}
for (const name of [
'OPENCODE_CONFIG_DIR',
'OPENCODE_DISABLE_AUTOUPDATE',
'OPENCODE_DISABLE_CLAUDE_CODE_SKILLS',
'OPENCODE_DISABLE_EMBEDDED_WEB_UI',
'OPENCODE_DISABLE_EXTERNAL_SKILLS',
'OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER',
'OPENCODE_DISABLE_LSP_DOWNLOAD',
'OPENCODE_DISABLE_MODELS_FETCH',
'OPENCODE_DISABLE_PROJECT_CONFIG',
'OPENCODE_DISABLE_SHARE'
]) {
const value = source[name]
if (value !== undefined) {
if (value.includes('\0')) {
throw new Error(`Invalid helper environment value for ${name}`)
}
environment[name] = value
}
}
return environment
}
+92 -4
View File
@@ -1,3 +1,4 @@
import { EventEmitter } from 'node:events'
import {
chmodSync,
existsSync,
@@ -21,8 +22,12 @@ import {
import {
createOpenCodeModelBridgeProviderConfig,
MODEL_BRIDGE_SDK_AUTH_SENTINEL,
ModelBridgeLoopbackProxy
ModelBridgeLoopbackProxy,
runOpenCodeModelBridgeHelper,
type ModelBridgeHelperSpawn
} from './model-bridge-helper'
import { createOpenCodeLaunchProfile } from './opencode-runtime-profile'
import { createRuntimeBundleTestFixture } from './runtime-bundle-test-fixture'
import {
createUnixModelBridgeExchange,
MODEL_BRIDGE_BROKER_SOCKET_NAME,
@@ -51,6 +56,89 @@ afterEach(() => {
})
describe('model bridge loopback helper', () => {
it.each(['ask', 'execute'] as const)(
'preserves the %s launch profile offline resources in the final OpenCode spawn',
async (workMode) => {
const fixture = await createRuntimeBundleTestFixture()
temporaryPaths.push(fixture.root)
const socketPath = join(fixture.root, 'bridge', 'model.sock')
const profile = createOpenCodeLaunchProfile({
...fixture,
workspaceDirectory: fixture.root,
workMode,
modelBridge: {
agentExecutablePath: join(fixture.root, 'agent', 'goodbuddy-agent'),
bridgeDirectory: join(fixture.root, 'bridge'),
socketPath,
policy: {
protocol: 'anthropic-messages',
model: 'private-model',
modelProfileDigest: `sha256:${'9'.repeat(64)}`,
supportsImageInput: false
}
}
})
const spawn = vi.fn<ModelBridgeHelperSpawn>(() => {
const child = new EventEmitter()
queueMicrotask(() => child.emit('close', 0, null))
return child
})
const resources = {
HOME: fixture.root,
PATH: join(fixture.root, 'tools'),
TMPDIR: join(fixture.root, 'tmp'),
XDG_CACHE_HOME: join(fixture.root, 'cache'),
XDG_CONFIG_HOME: join(fixture.root, 'config'),
XDG_DATA_HOME: join(fixture.root, 'data'),
XDG_STATE_HOME: join(fixture.root, 'state')
}
await expect(runOpenCodeModelBridgeHelper({
socketPath,
protocol: 'anthropic-messages',
model: 'private-model',
supportsImageInput: false,
workMode,
opencodeEntrypoint: join(fixture.bundleDirectory, 'bin', 'opencode'),
environment: {
...profile.env,
...resources,
OPENCODE_CONFIG_CONTENT: '{"model":"must-not-be-used"}',
OPENCODE_CONFIG: '/unrelated/config.json',
ANTHROPIC_API_KEY: 'must-not-be-forwarded',
NODE_OPTIONS: '--inspect'
},
spawn
})).resolves.toBe(0)
expect(spawn).toHaveBeenCalledOnce()
const [executable, args, options] = spawn.mock.calls[0]!
expect(executable).toBe(join(fixture.bundleDirectory, 'bin', 'opencode'))
expect(args).toEqual(['acp'])
expect(options).toMatchObject({ shell: false, stdio: 'inherit' })
expect(options.env).toMatchObject({
...resources,
LANG: profile.env.LANG,
LC_ALL: profile.env.LC_ALL,
OPENCODE_CONFIG_DIR: join(fixture.bundleDirectory, 'config', 'opencode'),
OPENCODE_DISABLE_AUTOUPDATE: '1',
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: '1',
OPENCODE_DISABLE_EMBEDDED_WEB_UI: '1',
OPENCODE_DISABLE_EXTERNAL_SKILLS: '1',
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: '1',
OPENCODE_DISABLE_LSP_DOWNLOAD: '1',
OPENCODE_DISABLE_MODELS_FETCH: '1',
OPENCODE_DISABLE_PROJECT_CONFIG: '1',
OPENCODE_DISABLE_SHARE: '1'
})
expect(options.env.OPENCODE_CONFIG).toBeUndefined()
expect(options.env.ANTHROPIC_API_KEY).toBeUndefined()
expect(options.env.NODE_OPTIONS).toBeUndefined()
const config = JSON.parse(options.env.OPENCODE_CONFIG_CONTENT!)
expect(config.model).toBe('goodbuddy-anthropic/private-model')
expect(config.permission).toBe(workMode === 'ask' ? 'ask' : 'allow')
expect(config.plugin).toHaveLength(1)
}
)
it('forwards only the bounded request contract and returns the response', async () => {
const exchange = vi.fn<ModelBridgeExchange>(
async () => validResponse
@@ -506,7 +594,7 @@ describe('model bridge loopback helper', () => {
}
)
it('leaves Execute provider permissions unchanged', () => {
it('explicitly allows Execute permissions instead of inheriting directory prompts', () => {
const config = createOpenCodeModelBridgeProviderConfig({
protocol: 'openai-responses',
model: 'private-model',
@@ -515,8 +603,8 @@ describe('model bridge loopback helper', () => {
workMode: 'execute'
})
expect(config).not.toHaveProperty('permission')
expect(config.agent).not.toHaveProperty('build')
expect(config.permission).toBe('allow')
expect(config.agent.build.permission).toBe('allow')
})
})
@@ -102,6 +102,10 @@ describe('OpenCode direct launch profile', () => {
)
})
expect(execute.args).not.toContain('--unshare-all')
expect(JSON.parse(execute.env.OPENCODE_CONFIG_CONTENT!)).toMatchObject({
permission: 'allow',
agent: { build: { permission: 'allow' } }
})
})
it('keeps Runtime arguments credential-free and rejects a changed manifest', () => {
@@ -338,7 +342,7 @@ function createFixture(): {
formatVersion: 2,
product: 'GoodBuddy',
runtimeId: 'opencode',
runtimeVersion: '1.18.9',
runtimeVersion: '1.18.29',
provider: 'opencode',
platform: 'linux',
architecture: 'x64',
@@ -373,7 +377,7 @@ function createFixture(): {
licenses: [
{
package: 'opencode-ai',
version: '1.18.9',
version: '1.18.29',
spdx: 'MIT',
path: 'licenses/opencode.txt'
}
+111 -133
View File
@@ -1,230 +1,208 @@
import {
dirname,
isAbsolute,
join,
relative,
resolve,
sep
} from 'node:path'
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import {
OPENCODE_REMOTE_RUNTIME_ENVIRONMENT_NAMES,
remoteRuntimeBundleManifestSchema,
type RemoteRuntimeBundleManifest
} from '../shared/remote-runtime-launch-contracts'
type RemoteRuntimeBundleManifest,
} from "../shared/remote-runtime-launch-contracts";
import {
modelBridgePolicySchema,
type ModelBridgePolicy
} from '../shared/model-bridge-contracts'
import {
assertAbsoluteManagedPath
} from './managed-paths'
type ModelBridgePolicy,
} from "../shared/model-bridge-contracts";
import { assertAbsoluteManagedPath } from "./managed-paths";
const OPEN_CODE_ASK_CONFIG = JSON.stringify({
permission: 'ask',
agent: {
build: {
permission: 'ask'
}
}
})
const OPEN_CODE_RUNTIME_ENVIRONMENT = Object.freeze({
OPENCODE_DISABLE_AUTOUPDATE: "1",
OPENCODE_DISABLE_CLAUDE_CODE_SKILLS: "1",
OPENCODE_DISABLE_EMBEDDED_WEB_UI: "1",
OPENCODE_DISABLE_EXTERNAL_SKILLS: "1",
OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "1",
OPENCODE_DISABLE_LSP_DOWNLOAD: "1",
OPENCODE_DISABLE_MODELS_FETCH: "1",
OPENCODE_DISABLE_PROJECT_CONFIG: "1",
OPENCODE_DISABLE_SHARE: "1",
});
export type OpenCodeLaunchProfile = {
executable: string
processExecutable: string
args: readonly string[]
cwd: string
env: Readonly<NodeJS.ProcessEnv>
workMode: 'ask' | 'execute'
}
executable: string;
processExecutable: string;
args: readonly string[];
cwd: string;
env: Readonly<NodeJS.ProcessEnv>;
workMode: "ask" | "execute";
};
export function createOpenCodeLaunchProfile(input: {
manifest: RemoteRuntimeBundleManifest
bundleDirectory: string
workspaceDirectory: string
workMode: 'ask' | 'execute'
manifest: RemoteRuntimeBundleManifest;
bundleDirectory: string;
workspaceDirectory: string;
workMode: "ask" | "execute";
modelBridge?: {
agentExecutablePath: string
bridgeDirectory: string
socketPath: string
policy: ModelBridgePolicy
}
agentExecutablePath: string;
bridgeDirectory: string;
socketPath: string;
policy: ModelBridgePolicy;
};
}): OpenCodeLaunchProfile {
const manifest = remoteRuntimeBundleManifestSchema.parse(
input.manifest
)
const manifest = remoteRuntimeBundleManifestSchema.parse(input.manifest);
if (
manifest.runtimeId !== 'opencode' ||
manifest.provider !== 'opencode' ||
manifest.entrypoint.identity !== 'opencode-acp' ||
manifest.runtimeId !== "opencode" ||
manifest.provider !== "opencode" ||
manifest.entrypoint.identity !== "opencode-acp" ||
manifest.entrypoint.argvPrefix.length !== 1 ||
manifest.entrypoint.argvPrefix[0] !== 'acp'
manifest.entrypoint.argvPrefix[0] !== "acp"
) {
throw new Error('Runtime manifest is not the fixed OpenCode ACP profile')
throw new Error("Runtime manifest is not the fixed OpenCode ACP profile");
}
if (
[...manifest.allowedEnvironmentNames].sort().join('\0') !==
[...OPENCODE_REMOTE_RUNTIME_ENVIRONMENT_NAMES]
.sort()
.join('\0')
[...manifest.allowedEnvironmentNames].sort().join("\0") !==
[...OPENCODE_REMOTE_RUNTIME_ENVIRONMENT_NAMES].sort().join("\0")
) {
throw new Error(
'OpenCode Runtime environment allowlist does not match the fixed profile'
)
"OpenCode Runtime environment allowlist does not match the fixed profile",
);
}
const bundleDirectory = assertAbsoluteManagedPath(
resolve(input.bundleDirectory)
)
resolve(input.bundleDirectory),
);
const workspaceDirectory = normalizedAbsolutePath(
input.workspaceDirectory,
'Workspace'
)
"Workspace",
);
const modelBridge =
input.modelBridge === undefined
? undefined
: {
agentExecutablePath: normalizedAbsolutePath(
input.modelBridge.agentExecutablePath,
'Agent executable'
"Agent executable",
),
agentInstallationDirectory: normalizedAbsolutePath(
dirname(input.modelBridge.agentExecutablePath),
'Agent installation'
"Agent installation",
),
bridgeDirectory: normalizedAbsolutePath(
input.modelBridge.bridgeDirectory,
'Model bridge'
"Model bridge",
),
socketPath: normalizedAbsolutePath(
input.modelBridge.socketPath,
'Model bridge socket'
"Model bridge socket",
),
policy: modelBridgePolicySchema.parse(
input.modelBridge.policy
)
}
policy: modelBridgePolicySchema.parse(input.modelBridge.policy),
};
if (
modelBridge !== undefined &&
(!isStrictDescendant(
modelBridge.bridgeDirectory,
modelBridge.socketPath
) ||
(!isStrictDescendant(modelBridge.bridgeDirectory, modelBridge.socketPath) ||
!isStrictDescendant(
modelBridge.agentInstallationDirectory,
modelBridge.agentExecutablePath
modelBridge.agentExecutablePath,
))
) {
throw new Error(
'Model bridge socket or Agent executable escapes its managed directory'
)
"Model bridge socket or Agent executable escapes its managed directory",
);
}
const executablePath = resolve(
bundleDirectory,
...manifest.entrypoint.path.split('/')
)
...manifest.entrypoint.path.split("/"),
);
if (!isStrictDescendant(bundleDirectory, executablePath)) {
throw new Error('OpenCode entrypoint escapes its bundle directory')
throw new Error("OpenCode entrypoint escapes its bundle directory");
}
const runtimeCommand =
modelBridge === undefined
? {
executable: executablePath,
args: [...manifest.entrypoint.argvPrefix]
args: [...manifest.entrypoint.argvPrefix],
}
: {
executable: join(
modelBridge.agentInstallationDirectory,
'node'
),
executable: join(modelBridge.agentInstallationDirectory, "node"),
args: [
join(
modelBridge.agentInstallationDirectory,
'lib',
'agent.cjs'
),
'model-bridge-helper',
'--socket-path',
join(modelBridge.agentInstallationDirectory, "lib", "agent.cjs"),
"model-bridge-helper",
"--socket-path",
modelBridge.socketPath,
'--protocol',
"--protocol",
modelBridge.policy.protocol,
'--model',
"--model",
modelBridge.policy.model,
'--supports-image-input',
modelBridge.policy.supportsImageInput ? 'true' : 'false',
'--work-mode',
"--supports-image-input",
modelBridge.policy.supportsImageInput ? "true" : "false",
"--work-mode",
input.workMode,
'--opencode-entrypoint',
executablePath
]
}
"--opencode-entrypoint",
executablePath,
],
};
const environment =
input.workMode === 'ask'
? {
...executeEnvironment(process.env, workspaceDirectory),
OPENCODE_CONFIG_CONTENT: OPEN_CODE_ASK_CONFIG
}
: executeEnvironment(process.env, workspaceDirectory)
const environmentBase = {
...executeEnvironment(process.env, workspaceDirectory),
...OPEN_CODE_RUNTIME_ENVIRONMENT,
OPENCODE_CONFIG_DIR: join(bundleDirectory, "config", "opencode"),
};
const permission = input.workMode === "ask" ? "ask" : "allow";
const environment = {
...environmentBase,
OPENCODE_CONFIG_CONTENT: JSON.stringify({
permission,
agent: { build: { permission } },
}),
};
return {
...runtimeCommand,
processExecutable: runtimeCommand.executable,
cwd: workspaceDirectory,
env: environment,
workMode: input.workMode
}
workMode: input.workMode,
};
}
function normalizedAbsolutePath(value: string, label: string): string {
if (
value.includes('\0') ||
!isAbsolute(value) ||
resolve(value) !== value
) {
throw new Error(`${label} path must be a normalized absolute path`)
if (value.includes("\0") || !isAbsolute(value) || resolve(value) !== value) {
throw new Error(`${label} path must be a normalized absolute path`);
}
return value
return value;
}
function executeEnvironment(
source: Readonly<NodeJS.ProcessEnv>,
workspaceDirectory: string
workspaceDirectory: string,
): Readonly<NodeJS.ProcessEnv> {
const home = usableEnvironmentValue(source.HOME)
? source.HOME!
: workspaceDirectory
: workspaceDirectory;
const defaults = {
HOME: home,
LANG: 'C.UTF-8',
LC_ALL: 'C.UTF-8',
PATH: '/usr/bin:/bin',
TMPDIR: '/tmp',
XDG_CACHE_HOME: join(home, '.cache'),
XDG_CONFIG_HOME: join(home, '.config'),
XDG_DATA_HOME: join(home, '.local', 'share'),
XDG_STATE_HOME: join(home, '.local', 'state')
} as const
const environment: NodeJS.ProcessEnv = { ...source }
LANG: "C.UTF-8",
LC_ALL: "C.UTF-8",
PATH: "/usr/bin:/bin",
TMPDIR: "/tmp",
XDG_CACHE_HOME: join(home, ".cache"),
XDG_CONFIG_HOME: join(home, ".config"),
XDG_DATA_HOME: join(home, ".local", "share"),
XDG_STATE_HOME: join(home, ".local", "state"),
} as const;
const environment: NodeJS.ProcessEnv = { ...source };
for (const [name, fallback] of Object.entries(defaults)) {
if (!usableEnvironmentValue(environment[name])) {
environment[name] = fallback
environment[name] = fallback;
}
}
return environment
return environment;
}
function usableEnvironmentValue(
value: string | undefined
): value is string {
return value !== undefined && value.length > 0 && !value.includes('\0')
function usableEnvironmentValue(value: string | undefined): value is string {
return value !== undefined && value.length > 0 && !value.includes("\0");
}
function isStrictDescendant(parent: string, candidate: string): boolean {
const child = relative(parent, candidate)
const child = relative(parent, candidate);
return (
child.length > 0 &&
child !== '..' &&
child !== ".." &&
!child.startsWith(`..${sep}`) &&
!isAbsolute(child)
)
);
}
@@ -0,0 +1,42 @@
import { runInNewContext } from 'node:vm'
import { describe, expect, it, vi } from 'vitest'
import { openCodeSubagentPluginSource } from './opencode-subagent-plugin'
describe('OpenCode ACP child event plugin', () => {
it('emits valid newline-delimited ACP updates only for owned child sessions', async () => {
const write = vi.fn()
const factory = runInNewContext(
`(${openCodeSubagentPluginSource().replace('export default ', '')})`,
{ process: { stdout: { write } } }
)
const hooks = await factory()
const event = {
type: 'message.part.updated',
properties: {
sessionID: 'parent',
part: {
type: 'tool', tool: 'task', callID: 'task',
state: { metadata: { sessionId: 'child' } }
}
}
}
await hooks.event({ event })
await hooks.event({ event: {
type: 'message.part.delta', properties: { sessionID: 'child', delta: 'Inspecting' }
} })
await hooks.event({ event: {
type: 'message.part.delta', properties: { sessionID: 'unrelated', delta: 'Private' }
} })
expect(write).toHaveBeenCalledTimes(2)
for (const [line] of write.mock.calls) {
expect(line.endsWith('\n')).toBe(true)
expect(JSON.parse(line)).toMatchObject({
method: 'session/update',
params: { sessionId: 'parent', update: {
sessionUpdate: 'tool_call_update', toolCallId: 'task',
_meta: { goodbuddySubagentEvent: { properties: expect.any(Object) } }
} }
})
}
})
})
@@ -0,0 +1,56 @@
/**
* OpenCode's ACP adapter omits native child sessions. This plugin forwards
* their SDK events as ACP metadata on the owning Task, using the same stdio
* stream and prompt lifetime as the built-in adapter.
*/
export function openCodeSubagentPluginSource(): string {
return `export default ${plugin.toString()}`
}
function plugin() {
const tasks = new Map<string, { sessionId: string; callId: string }>()
return Promise.resolve({
event: async ({ event }: { event: {
type: string
properties: {
sessionID?: string
part?: {
type: string
tool?: string
callID?: string
state?: {
metadata?: { sessionId?: string }
}
}
}
} }) => {
const properties = event.properties
const part = properties.part
let owner = properties.sessionID ? tasks.get(properties.sessionID) : undefined
if (
!owner && event.type === 'message.part.updated' &&
part?.type === 'tool' && part.tool === 'task' &&
part.callID && properties.sessionID
) {
owner = { sessionId: properties.sessionID, callId: part.callID }
const child = part.state?.metadata?.sessionId
if (child) tasks.set(child, owner)
}
if (!owner || ![
'message.updated', 'message.part.updated', 'message.part.delta'
].includes(event.type)) return
process.stdout.write(JSON.stringify({
jsonrpc: '2.0',
method: 'session/update',
params: {
sessionId: owner.sessionId,
update: {
sessionUpdate: 'tool_call_update',
toolCallId: owner.callId,
_meta: { goodbuddySubagentEvent: event }
}
}
}) + '\n')
}
})
}
+19 -3
View File
@@ -67,23 +67,39 @@ export async function createLinuxPeerIdentityProvider(
byteOrder?: 'BE' | 'LE'
loadKoffi?: () => Promise<Koffi>
} = {}
): Promise<LinuxPeerIdentityProvider> {
): Promise<UnixPeerIdentityProvider> {
const platform = options.platform ?? process.platform
const architecture = options.architecture ?? arch()
const byteOrder = options.byteOrder ?? endianness()
if (
platform !== 'linux' ||
(platform !== 'linux' && !(platform === 'darwin' && architecture === 'arm64')) ||
!SUPPORTED_ARCHITECTURES.has(architecture) ||
byteOrder !== 'LE'
) {
throw unavailable(
'SO_PEERCRED is certified only on little-endian Linux x64 and arm64'
'Unix peer credentials require little-endian Linux or macOS arm64'
)
}
try {
const koffi =
await (options.loadKoffi?.() ??
import('koffi').then((module) => module.default))
if (platform === 'darwin') {
const library = koffi.load('/usr/lib/libSystem.B.dylib')
const getpeereid = library.func('getpeereid', 'int', [
'int', 'void *', 'void *'
])
return {
async getPeerIdentity(socket) {
const uid = Buffer.alloc(4)
const gid = Buffer.alloc(4)
if (getpeereid(socketDescriptor(socket), uid, gid) !== 0) {
throw new Error(`getpeereid failed with errno ${koffi.errno()}`)
}
return { uid: uid.readUInt32LE() }
}
}
}
return new LinuxPeerIdentityProvider(createNativeBinding(koffi))
} catch (error) {
if (error instanceof AgentUnsupportedError) {
+66
View File
@@ -29,6 +29,7 @@ import { InstallationChallengeVerifier } from './installation-challenge'
import {
attachRelay,
PrivateEndpoint,
probeAuthenticatedEndpoint,
type PeerIdentity
} from './private-endpoint'
import { AgentProtocolServer } from './protocol-server'
@@ -51,6 +52,57 @@ afterEach(() => {
})
describe('private endpoint lifecycle', () => {
runOnUnix.each(['-', '_'])('accepts a valid HMAC beginning with %s', async (prefix) => {
const root = privateTemporaryDirectory()
const verifier = new InstallationChallengeVerifier(Buffer.alloc(32, 7))
const attached = vi.fn()
const endpoint = new PrivateEndpoint({
socketPath: join(root, 'agent.sock'),
peerIdentity: currentPeerIdentity(), challenge: verifier,
controllers: new ControllerRegistry(), ...endpointIdentity, onAttach: attached
})
await endpoint.listen()
const client = createConnection(join(root, 'agent.sock'))
try {
await onceConnected(client)
const challenge = await readPacket(client)
let clientNonce = ''
let response = ''
for (let index = 0; index < 4096 && !response.startsWith(prefix); index++) {
clientNonce = `client-${index}`
response = verifier.createResponse({
serverNonce: String(challenge.serverNonce), clientNonce, controllerId: 'controller-a'
})
}
expect(response.startsWith(prefix)).toBe(true)
await writePacket(client, {
type: 'response', version: 1, controllerId: 'controller-a', clientNonce,
protocol: AGENT_PROTOCOL_VERSION, response
})
expect(attachWelcomeSchema.parse(await readPacket(client)).installationId).toBe('installation-a')
await vi.waitFor(() => expect(attached).toHaveBeenCalledOnce())
} finally {
client.destroy()
await endpoint.close()
}
})
runOnUnix('rejects a failed health handshake instead of leaving an unsettled read', async () => {
const root = privateTemporaryDirectory()
const socketPath = join(root, 'agent.sock')
const endpoint = new PrivateEndpoint({
socketPath, peerIdentity: currentPeerIdentity(),
challenge: new InstallationChallengeVerifier(Buffer.alloc(32, 7)),
controllers: new ControllerRegistry(), ...endpointIdentity, onAttach: vi.fn()
})
await endpoint.listen()
try {
await expect(probeAuthenticatedEndpoint({
socketPath, secret: Buffer.alloc(32, 8), ...endpointIdentity
})).rejects.toThrow('closed during handshake')
} finally { await endpoint.close() }
})
runOnUnix(
'never pre-unlinks an endpoint that may still be live',
async () => {
@@ -346,6 +398,20 @@ describe('private endpoint lifecycle', () => {
})
describe('attach relay preface validation', () => {
it.each(['before', 'during'])('rejects a stream destroyed %s its preface arrives', async (when) => {
const input = new PassThrough()
if (when === 'before') input.destroy()
const relay = attachRelay({
socketPath: resolve('missing-agent.sock'), secret: Buffer.alloc(32, 7),
input, output: new PassThrough()
})
const rejection = expect(relay).rejects.toThrow('closed during preface')
if (when === 'during') input.destroy()
await rejection
expect(input.listenerCount('readable')).toBe(0)
expect(input.listenerCount('close')).toBe(0)
})
it('rejects unknown Main preface fields before opening an endpoint', async () => {
const input = new PassThrough()
const output = new PassThrough()
+15 -2
View File
@@ -51,7 +51,8 @@ const challengeResponsePacketSchema = z
controllerId: agentIdentifierSchema,
clientNonce: agentIdentifierSchema,
protocol: protocolVersionSchema,
response: agentIdentifierSchema
// A SHA-256 HMAC is canonical Base64URL, not an application identifier.
response: z.string().regex(/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/u)
})
.strict()
@@ -400,7 +401,7 @@ export async function probeAuthenticatedEndpoint(options: {
const challenge = challengePacketSchema.parse(await readPacket(socket))
const verifier = new InstallationChallengeVerifier(options.secret)
const controllerId = 'lifecycle-health'
const clientNonce = randomBytes(24).toString('base64url')
const clientNonce = `health-${randomBytes(24).toString('base64url')}`
await writePacket(socket, {
type: 'response',
version: 1,
@@ -559,6 +560,9 @@ async function readExactly(socket: Socket, byteLength: number): Promise<Buffer>
remaining -= chunk.byteLength
continue
}
if (socket.destroyed || socket.readableEnded) {
throw new Error('Attach socket closed during handshake')
}
await new Promise<void>((resolve, reject) => {
const onReadable = (): void => {
cleanup()
@@ -575,10 +579,12 @@ async function readExactly(socket: Socket, byteLength: number): Promise<Buffer>
const cleanup = (): void => {
socket.off('readable', onReadable)
socket.off('end', onEnd)
socket.off('close', onEnd)
socket.off('error', onError)
}
socket.once('readable', onReadable)
socket.once('end', onEnd)
socket.once('close', onEnd)
socket.once('error', onError)
})
}
@@ -588,6 +594,8 @@ async function readExactly(socket: Socket, byteLength: number): Promise<Buffer>
async function readExactlyFromStream(
stream: NodeJS.ReadableStream & {
read(size?: number): Buffer | null
destroyed?: boolean
readableEnded?: boolean
},
byteLength: number
): Promise<Buffer> {
@@ -601,6 +609,9 @@ async function readExactlyFromStream(
remaining -= bytes.byteLength
continue
}
if (stream.destroyed || stream.readableEnded) {
throw new Error('Attach relay input closed during preface')
}
await new Promise<void>((resolve, reject) => {
const onReadable = (): void => {
cleanup()
@@ -617,10 +628,12 @@ async function readExactlyFromStream(
const cleanup = (): void => {
stream.removeListener('readable', onReadable)
stream.removeListener('end', onEnd)
stream.removeListener('close', onEnd)
stream.removeListener('error', onError)
}
stream.once('readable', onReadable)
stream.once('end', onEnd)
stream.once('close', onEnd)
stream.once('error', onError)
})
}
+2 -2
View File
@@ -431,7 +431,7 @@ describe('AgentProtocolServer connection bounds', () => {
it('advertises only a verified Runtime with ACP control and data handlers', async () => {
const runtime = {
runtimeId: 'opencode',
version: '1.18.9',
version: '1.18.29',
bundleDigest: `sha256:${'b'.repeat(64)}`,
acpCapabilitiesDigest: `sha256:${'c'.repeat(64)}`,
sessionLoad: false,
@@ -481,7 +481,7 @@ describe('AgentProtocolServer connection bounds', () => {
it('advertises the critical model bridge only with blob composition', async () => {
const runtime = {
runtimeId: 'opencode',
version: '1.18.9',
version: '1.18.29',
bundleDigest: `sha256:${'b'.repeat(64)}`,
acpCapabilitiesDigest: `sha256:${'c'.repeat(64)}`,
sessionLoad: false,
@@ -25,13 +25,13 @@ afterEach(() => {
describe('Runtime bundle registry', () => {
it('registers and resolves the current signed digest atomically', () => {
const fixture = createRegistry()
const verified = runtimeBundle(fixture, '1.18.9', 'a')
const verified = runtimeBundle(fixture, '1.18.29', 'a')
const entry = fixture.registry.register(verified)
expect(entry).toMatchObject({
runtimeId: 'opencode',
runtimeVersion: '1.18.9',
runtimeVersion: '1.18.29',
architecture: 'x64',
bundleDigest: verified.manifest.bundleDigest
})
@@ -54,7 +54,7 @@ describe('Runtime bundle registry', () => {
it('reuses an exact bundle and replaces it with another signed identity', () => {
const fixture = createRegistry()
const first = runtimeBundle(fixture, '1.18.9', 'a')
const first = runtimeBundle(fixture, '1.18.29', 'a')
const second = runtimeBundle(fixture, '1.19.0', 'b')
expect(fixture.registry.register(first)).toEqual(
@@ -87,7 +87,7 @@ describe('Runtime bundle registry', () => {
current: [{
runtimeId: 'opencode',
provider: 'opencode',
runtimeVersion: '1.18.9',
runtimeVersion: '1.18.29',
releaseSequence: 3,
architecture: 'x64',
signingKeyId: 'test-key',
@@ -107,7 +107,7 @@ describe('Runtime bundle registry', () => {
formatVersion: 1,
current: [{
runtimeId: 'opencode',
runtimeVersion: '1.18.9',
runtimeVersion: '1.18.29',
architecture: 'x64',
bundleDigest: `sha256:${'a'.repeat(64)}`,
manifestDigest: `sha256:${'b'.repeat(64)}`,
@@ -124,7 +124,7 @@ describe('Runtime bundle registry', () => {
const activator = new RuntimeBundleRegistry({
runtimeRoot: fixture.runtimeRoot
})
const first = runtimeBundle(fixture, '1.18.9', 'a')
const first = runtimeBundle(fixture, '1.18.29', 'a')
const firstEntry = activator.register(first)
expect(fixture.registry.current('x64')).toEqual([firstEntry])
@@ -31,7 +31,7 @@ export const TEST_REMOTE_RUNTIME_LOCK: RemoteRuntimeLock = {
formatVersion: 1,
runtimes: {
opencode: {
version: '1.18.9',
version: '1.18.29',
provider: 'opencode',
entrypoint: 'bin/opencode',
entrypointIdentity: 'opencode-acp',
@@ -87,7 +87,7 @@ export async function createRuntimeBundleTestFixture(
formatVersion: 2,
product: 'GoodBuddy',
runtimeId: 'opencode',
runtimeVersion: '1.18.9',
runtimeVersion: '1.18.29',
provider: 'opencode',
platform: 'linux',
architecture: 'x64',
@@ -127,7 +127,7 @@ export async function createRuntimeBundleTestFixture(
licenses: [
{
package: 'opencode-ai',
version: '1.18.9',
version: '1.18.29',
spdx: 'MIT',
path: 'licenses/opencode.txt'
}
@@ -188,7 +188,7 @@ describe('Remote Runtime bundle verifier', () => {
await expect(capabilities()).resolves.toEqual([
{
runtimeId: 'opencode',
version: '1.18.9',
version: '1.18.29',
bundleDigest: fixture.manifest.bundleDigest,
acpCapabilitiesDigest:
fixture.manifest.acpCapabilitiesDigest,
@@ -236,7 +236,7 @@ describe('Remote Runtime bundle verifier', () => {
options
)
).resolves.toMatchObject({
manifest: { runtimeVersion: '1.18.9' }
manifest: { runtimeVersion: '1.18.29' }
})
writeFileSync(
+1 -1
View File
@@ -101,7 +101,7 @@ describe('production Runtime composition', () => {
await expect(protocol.runtimes()).resolves.toEqual([
{
runtimeId: 'opencode',
version: '1.18.9',
version: '1.18.29',
bundleDigest: fixture.manifest.bundleDigest,
acpCapabilitiesDigest:
fixture.manifest.acpCapabilitiesDigest,
+2 -2
View File
@@ -225,7 +225,7 @@ export async function createProductionRuntimeProtocol(
try {
if (
verificationEnvironment === 'production' &&
process.platform === 'linux'
(process.platform === 'linux' || process.platform === 'darwin')
) {
await (
options.reconcileOrphanedProcesses ??
@@ -436,7 +436,7 @@ export function derivePrivateModelBridgeDirectory(
controllerGeneration?: number
}
): string {
if (process.platform === 'linux') {
if (process.platform === 'linux' || process.platform === 'darwin') {
const stateDirectory = resolve(stateDirectoryInput)
ensurePrivateDirectory(stateDirectory, { create: false })
const userRoot = ensurePrivateTemporaryRoot(
@@ -1,5 +1,6 @@
import { readFile, readlink } from 'node:fs/promises'
import { posix } from 'node:path'
import { darwinBootId, inspectDarwinProcess } from './darwin-process-identity'
export type LinuxRuntimeProcessIdentity = {
bootId: string
@@ -72,6 +73,19 @@ export async function readLinuxRuntimeProcessIdentity(
options: { procRoot?: string; bootId?: string } = {}
): Promise<LinuxRuntimeProcessIdentity> {
const pid = positiveInteger(pidInput, 'PID')
if (process.platform === 'darwin') {
const identity = inspectDarwinProcess(pid)
if (identity === undefined) {
throw Object.assign(new Error('Runtime process no longer exists'), { code: 'ESRCH' })
}
return {
bootId: options.bootId ?? darwinBootId(),
pid,
startTimeTicks: BigInt(identity.starttime),
processGroupId: identity.processGroupId,
executablePath: identity.executablePath
}
}
const procRoot = options.procRoot ?? '/proc'
if (!posix.isAbsolute(procRoot) || procRoot.includes('\0')) {
throw new Error('Linux proc root must be absolute')

Some files were not shown because too many files have changed in this diff Show More