Skip to content

build: migrate the package manager from pnpm to Bun - #4

Merged
7723qqq merged 42 commits into
mainfrom
feat/bun-package-manager
Aug 24, 2026
Merged

build: migrate the package manager from pnpm to Bun#4
7723qqq merged 42 commits into
mainfrom
feat/bun-package-manager

Conversation

@7723qqq

@7723qqq 7723qqq commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Related Issue

无关联 issue——本仓库(fork)的基础设施改造,源自会话内的迁移评估与批准计划(pnpm → Bun 包管理器切换,独立于进行中的发布引擎 SEA→Bun 迁移 feat/bun-migration)。

Problem

仓库此前同时依赖两套工具链:发布产物已由 Bun 编译(build-bun.mjs),但开发/CI/Nix 的包管理仍是 pnpm。评估实验(冷装 4.2s vs 分钟级、热装 0.42s)确认收益集中在安装与开发迭代速度,且四个必改项均有明确解法,故按批准的两阶段计划完成整体切换。

What changed

阶段一(共存期)

  • 新增 bunfig.toml(钉死 hoisted linker)与 bun.lock
  • 完整 overrides 表从 pnpm-workspace.yaml 迁入根 package.json,双 PM 解析一致
  • trustedDependencies 替代 onlyBuiltDependencies;node-gyp 显式 devDep(Bun 不自供)
  • ssh2 的 optionalDependencies(cpu-features/nan)改用 patches/ssh2@1.17.0.patch 剔除——pnpm 的 - 移除语法在纯 Bun 下不生效,此补丁保住原有供应链加固语义

阶段二(切换)

  • 根 scripts 与 Makefile 全部改为 bun 等价物;删除 packageManager 字段
  • ci / release / _native-build / pkg-pr-new / docs-deploy 五个工作流改为 oven-sh/setup-bun@v2(钉 1.4.0)+ bun.lock 键缓存 + bun install --frozen-lockfile
  • 删除 pnpm-workspace.yaml、pnpm-lock.yaml、.npmrc
  • flake.nix:自写 Bun 缓存 FOD 替代 fetchPnpmDeps/pnpmConfigHook,钉上游 Bun 1.4.0(nixpkgs-25.11 仅 1.3.3);移除 workspaceNames;引擎仍为 SEA
  • check-nix-workspace.mjs 改读 package.json workspaces、仅校验 workspacePaths
  • 全仓文档/AGENTS/skills/启动器的 pnpm 指令清扫;apps/kimi-web 保持自管 pnpm 不动

验证

  • 干净环境 bun install --frozen-lockfile:1680 包 3.6s;全量 vitest 16176 用例通过(1 例负载性 flake 单跑复现即绿);typecheck/build 全绿
  • bun run build:native:bun + 冒烟(版本一致性/native asset/workers/PTY binding)通过
  • 已知待办:flake.nix 的 bunDeps FOD hash 为占位值,首次 CI 运行会以 got-hash 失败并由 nix-build bot 评论真实哈希,届时回填即可

Checklist

  • I have read the CONTRIBUTING document.
  • I have linked a related issue (fork infra change; no upstream issue applicable).
  • Added tests where behavior changed (release-artifacts bun-section completeness; layout-robust tree-sitter harness).
  • Ran gen-changesets skill — decision: no changeset, the package-manager switch is not user-perceivable in the shipped artifact.
  • Ran doc updates inline (full repo sweep included above).

luozihao added 30 commits August 23, 2026 21:59
TypeScript 6 and tsgolint treat "baseUrl" as an error-level deprecation.
Resolve #/* subpath imports through per-package paths mappings instead,
which also makes the source-first workspace resolvable under Bun 1.4.
Detect the runtime at spawn time: under Bun use the built-in
Bun.Terminal PTY (decoding chunks to strings and taking the real
subprocess exit code from the exited promise), elsewhere keep the
existing node-pty path. The TerminalProcess contract is unchanged.
Mechanical fixes only (import dedup, catch parameter naming, redundant
type conversions), audited for semantics; agent-core-v2 excluded because
its module-registration side effects depend on import statement order.
Structured log keys and wire payload keys renamed by catch-error-name
are restored explicitly.
Add build:native:bun alongside the existing SEA pipeline: reuse the
tsdown bundles and asset collectors, stage all embedded files with a
.bin suffix (bun compile otherwise parses .cjs/.json imports as
modules), generate a key-to-path manifest module, and compile with
bun build --compile. At runtime a Bun-backed NativeAssetSource feeds
the same extraction and validation path used for SEA, so workers,
web assets, and native bindings behave identically.

Also migrate pnpm overrides from package.json (ignored since pnpm
10.33) into pnpm-workspace.yaml to keep the hardened dependency
floor active.
Add a build-bun workflow input (default false) that builds, smokes,
and packages the Bun single-file binary on ubuntu-24.04. The build
script now maps KIMI_CODE_BUILD_TARGET to bun compile targets and
reuses the shared sign/checksum step for artifact parity.
Convert || to ?? only at sites whose declared types exclude empty
strings, zero, and false; wire-parsed and index-access sites stay on
|| deliberately.
Replace non-null assertions with explicit guards where values cross
trust boundaries: RESP command arity from clients, worker postMessage
results, disk-ref reads through optional handles, and empty-token
caller input. Hot-path skiplointer chasing keeps its structural
invariants documented by loop bounds.
Union narrowing over real payload types for TUI session replay,
Event2<unknown> for event bus seams, contravariance-correct
constructor params in the channel registry, and typed React global
lookup in i18n-shared. The public SDK SessionMeta.custom record stays
as-is (consumer-visible contract).
The failure path deactivated the agent context before disposing the
scope, so the activity view's final dispose-time publish hit the event
bus with no active lifecycle context and crashed teardown — masking
whatever caused the failure. Dispose first, then deactivate, matching
the order the normal close path already uses.

Verified end to end: a headless prompt against a mock OpenAI-compatible
endpoint now reports its actual configuration error instead of crashing,
and completes cleanly under both Node and Bun once configured.
Config errors during agent creation must surface their real semantics
(a missing max_context_size) rather than being masked. The lifecycle
ordering fix is verified manually against the CLI spawn path; this
suite locks the SDK-level contract.
Replace non-null assertions with the package's existing validation and
error conventions: kosong.addProvider validates through the facade's
zod schema, and memory-transport stream dispatch raises REQUEST_INVALID
when a source fails to start. A node-sdk lazy-init switches to ??=.
Narrow ServiceIdentifier maps to unknown, type Promisify/Promisable via Promise<unknown>, give provideUnit's overload a precise second-parameter type, and lift implementation notes from inline comments onto the exported symbols they document (kosong error classifiers, FetchURLTool, event dispatcher folds).
Bun.Terminal delivers raw bytes, so decode them with a streaming TextDecoder instead of assuming chunk-aligned UTF-8; flush the decoder before the exit event so a trailing truncated sequence surfaces as U+FFFD like node-pty would report it, and stop emitting empty strings for chunks that only carry part of a multi-byte character.
Describe resume_agent_ids as the flat agent_id-to-prompt record it is, warn against arrays and {{item}} keys, and refresh the toolset hash snapshots plus pinned token counts that the longer description shifts.
…dings

Bun ignores Module._load overrides and routes .node specifiers straight to dlopen, so materialize pi-tui's platform helper next to the extracted entry where its own candidate search looks, and do the same for the whole node-pty binding tree. On Node SEA the module hook now also redirects node-pty's relative requires into the native cache. Register node-pty in the native-dep registry, preserve POSIX file modes through asset extraction so darwin's spawn-helper stays executable, and dlopen the binding in the release smoke on every target.
Detect packaged Bun installs via the embedded-asset marker (Bun lacks node:sea) and report {native,kind} instead of a boolean. The release manifest gains an optional bun section; a Bun packaged client stages from it and refuses to update when the release ships no Bun build, rather than silently swapping itself to the Node SEA binary. Staged metadata records the engine.
Default LocalFetchURLProvider to the vendored undici fetch instead of globalThis.fetch: the pinned-DNS dispatcher option only exists in undici, and on runtimes whose global fetch is not undici (Bun) it would be ignored — silently dropping DNS pinning and re-resolving through whatever resolver the runtime prefers.
Render a Runtime row (e.g. 'bun · rust') from the packaging detection and the native-tools probe, wiring up the previously dead nativeTools option so rollout builds are identifiable at a glance.
The content-addressed extraction dir accumulates one main.cjs per upgraded version; sweep entries untouched for over a day at startup (young files may belong to a concurrently starting older binary). Add scripts/native/bench-native.mjs to compare cold-ish start wall time across packaged builds.
Run the Bun pipeline through the same six-target matrix as SEA with release-profile parity (built-in catalog, APPLE_SIGNING_IDENTITY, codesign self-check), package it under engine-suffixed artifact names, aggregate bun checksums into the manifest's optional bun section (and refuse a platforms-less manifest), and enable build-bun on releases and manual bundles. Bytecode is on by default with KIMI_CODE_BUN_NO_BYTECODE as the escape hatch.
The loader builds its candidate paths by string concatenation, so separator runs arrive doubled (prebuilds/linux-x64//pty.node) and the redirect pattern never matched — caught by the new PTY-binding smoke on a real linux-x64 SEA build. Assert the binding marker explicitly in the smoke output.
…sessions

Bundled copies of node-pty cannot work in single-file builds: their binding requires resolve against the bundle file, which exists at neither the SEA nor the Bun runtime layout — and Module._load overrides are never consulted inside the SEA runtime, so loader-hook redirects are dead code there. Load through the injected asset-cache resolver instead, addressing the package entry file directly because Bun does not implement self-reference resolution from a package.json path.
Packaged single-file builds inject __kimi_getNativePackageRoot; consult it before the on-disk candidate paths so darwin/win32 helpers load in SEA and Bun builds, where neither moduleDir nor execPath candidates contain the helper.
Keep node-pty external to the native bundle (like kimi-native-tools), ship its full package with per-target bindings through the asset pipeline, and resolve it at the call sites via a shared loader that prefers the extracted cache — verified end-to-end by the PTY-binding smoke on real SEA and Bun linux-x64 builds. Remove installNativeModuleHook entirely: the SEA runtime never consults Module._load for bundle-internal requires, so its redirects never fired; drop the Bun materializers that depended on the same premise.
Trigger the manual native bundle workflow on pushes to feat/bun-migration
(docs-only changes excluded) so the six-platform Bun matrix is verified
automatically. Keep workflow_dispatch.

Gracefully skip macOS code signing when the APPLE_* secrets are not
configured (e.g. on this fork): relay secret presence through a job-level
env (the secrets context is unavailable in step-level if expressions) and
fall back to the local-profile build instead of failing in keychain setup.
…pi linking

The asset collector requires a prebuilt kimi-agent .node next to the
package, but no CI step ever built one — local builds only passed on
machines with a leftover untracked artifact. Add the build step to both
matrix jobs after kimi-native-tools.

macOS cdylib linking fails with undefined _napi_* symbols because the
fork's custom build.rs does not emit dynamic_lookup link args; add the
standard apple-target rustflags to both Rust addons' cargo config.
Compiled Bun executables would autoload .env / bunfig.toml from the
user's cwd at runtime, silently diverging from Node/SEA behavior —
disable both via the compile flags (verified against bun 1.4.0).

Bytecode becomes opt-in via KIMI_CODE_BUN_ENABLE_BYTECODE=1: measured
no startup gain on this pipeline (only the entry shim is bytecode'd;
main.cjs stays outside the graph) while adding size and locking the
artifact to the exact Bun build version. Correct the stale top-level
await note in bun-entry.ts: the CJS default output format is what
constrains TLA, ESM output has supported it since Bun 1.3.9.
luozihao added 11 commits August 24, 2026 16:53
The two-line pnpm-workspace.yaml in apps/kimi-code hijacked workspace
resolution under pnpm >= 11, breaking 'pnpm --filter
@moonshot-ai/kimi-code run test' from every location. The override and
its self-link dependency were dead code (dropped from the lockfile
importer long ago); removing the file restores the documented test
entry from both the repo root and the app directory.
Fold the experimental native-bun section into a broader Bun migration
section covering the runtime integration work (node-pty/pi-tui asset
cache loading, dual-runtime terminal sessions, engine-aware self
update, cross-runtime fetch semantics), state verified-platform status
honestly, and lay out the roadmap: full CI matrix validation ->
bytecode/TLA constraint cleanup -> default engine switch and SEA
retirement.
The smoke test requires <binDir>/native/<os>/prebuilds/<arch>/<helper>.node,
and the runtime loader keeps it as a candidate path, but neither the SEA
nor the Bun pipeline ever produced that layout after the bun materializer
was removed — collection recorded the helper in the manifest while
nothing copied it next to the binary, so every darwin/win32 job failed
at smoke with a missing-module error (linux ships no helper and passed).

Add an exec-side staging step derived from the collected manifest: map
pi-tui's native/**.node entries onto the executable-side relative layout
and copy them into place for both pipelines. 02-sea-blob now returns the
collection result so build.mjs reuses it instead of collecting twice.
Two related signing gaps: bun --compile may emit an already-signed
binary on darwin-x64, making the plain 'codesign --sign -' step fail
with 'is already signed'; and SEA blobs that skip the numbered-script
chain were never re-signed after postject injection, leaving a stale
signature that macOS kernels reject with a silent early kill. Pass
--force on adhoc signing and unconditionally adhoc re-sign darwin
binaries right after NODE_SEA_BLOB injection; release-profile identity
signing runs later and overrides it.
Node's darwin SEA runtime resolves the injected blob via
getsectdata("NODE_SEA", "NODE_SEA_BLOB"), and postject defaults to a
__POSTJECT segment name when none is given — the resulting Mach-O dies
silently right after exec, which is exactly how the darwin local-profile
SEA jobs failed while linux/win32 (and the whole Bun track) passed.
Upstream avoids this by passing --macho-segment-name NODE_SEA to the
postject CLI; pass machoSegmentName through the API instead.
- Remove the standalone ReadMediaFile tool; Read handles media directly
  (mediaReadContext, execute-media-read), with legacy-name aliasing kept
  for permission rules and session replays
- Bundle the runtime into the Bun compile entry graph: drop the /tmp
  extraction + dynamic import startup path; add bun-assets.setup.ts
- produce-manifest.mjs now fails when the manifest's bun section is
  missing any supported target; _native-build.yml gains a build-sea input
- Update CONTRIBUTING (en/zh) bytecode wording to match the new pipeline
- Fix stale test expectations (image placeholder copy, mcp test harness)
- bunfig.toml pins the hoisted linker; bun.lock generated and committed
- Full overrides table migrated from pnpm-workspace.yaml into
  package.json so both package managers resolve identical versions
- trustedDependencies allowlist replaces pnpm's onlyBuiltDependencies;
  node-gyp added as a devDependency for node-pty's install script
- ssh2 optionalDependencies (cpu-features, nan) removed via
  patches/ssh2@1.17.0.patch, preserving the supply-chain hardening that
  previously relied on pnpm's '-' override syntax
- tree-sitter-bash differential harness resolves the reference wasm via
  createRequire instead of a pnpm-layout path
- Fix pre-existing type error in executeMediaRead header parameter
- en/zh docs: ReadMediaFile references updated to the merged Read tool
- Add read-folds-in-media changeset for the upcoming release
- Reformat touched permission/read modules with oxfmt
- Root scripts and Makefile: pnpm -r/--filter/-C replaced with
  bun run --workspaces/--filter and cd chains; packageManager field
  removed
- All release workflows (ci, release, _native-build, pkg-pr-new,
  docs-deploy) install with bun install --frozen-lockfile via
  oven-sh/setup-bun@v2 pinned to 1.4.0 plus a bun.lock-keyed cache;
  the typecheck job runs tsgo through bunx
- Delete pnpm-workspace.yaml, pnpm-lock.yaml, and .npmrc; Bun is now
  the only supported package manager
…oling

- Root AGENTS.md, CONTRIBUTING (en/zh), READMEs, package AGENTS/READMEs,
  .changeset/README, and agent skills now instruct Bun equivalents
- check-nix-workspace.mjs reads workspaces from root package.json
  (pnpm-workspace.yaml is gone) and validates workspacePaths only;
  flake.nix no longer carries a workspaceNames list
- Generated manifest headers and their generators updated in lockstep
- Functional script fixes: node-sdk build:dts, vis dev concurrently
  commands, build-vis-asset vite invocation, vscode packaging error
  hints, native asset install hints
@github-actions

github-actions Bot commented Aug 24, 2026

Copy link
Copy Markdown

❌ Nix build failed

Hash mismatch in bunDeps:

Hash
specified sha256-P450+LKDYkRyk7OZ2mSOX0/RwtbivwR5ZksN8FM6+TU=
got sha256-/l9+6gLtsShbsVCDuf/zHzNbzM16K+IwwNLRykbBX4w=

Please update flake.nix with the got hash.

@7723qqq
7723qqq merged commit d48a7f6 into main Aug 24, 2026
6 of 14 checks passed
7723qqq added a commit that referenced this pull request Aug 31, 2026
…ne paths

P38 G-6 item #4: the napi and stdio paths built NativeToolCallbacks with
plan_guard: None, so plan-mode Write/Edit interception only existed in the
REPL — the permission policy chain has no plan concept and the host's
check_permission cannot cover it.

The guard now reads the host's plan state through the existing state bridge
(host/state_read domain=plan) before every guarded native call — zero wire
changes, and mid-turn EnterPlanMode/ExitPlanMode are naturally covered
(a per-turn snapshot would go stale there). PlanGuard becomes async; the
guard denial now emits the tool.native event like the permission denial
does, so the transcript records the card's terminal state.

The decision lives in tools::plan_mode::plan_denial (v2
AgentPlanService.guardToolExecution semantics: Write/Edit restricted to the
plan file, TaskStop/CronCreate/CronDelete denied, component-wise path
comparison for mixed separators). Unguarded tools exit before any state
round-trip; a failed state read fails open with the permission layer still
in charge. The shared function also fixes a REPL gap: CronCreate/CronDelete
were not denied there before.

Verified: cargo test --lib 824 green (8 new plan_denial unit tests, REPL
cron cases), stdio integration 10/10 including a new end-to-end case
(plan active + native Write outside the plan file -> guard vetoes before
the permission round-trip, exactly one plan state read, tool.native event
carries the denial, nothing lands on disk), clippy 0 warnings, fmt clean.
7723qqq added a commit that referenced this pull request Sep 1, 2026
v2 goalAgentRuntime 的 CreateGoal 启动审批与陈旧 goal 拒绝此前对原生
执行完全失效(goal 静默启动、旧轮可改已变更的 goal)。本批在引擎内补齐:

- tools/goal_guard.rs(新增):GoalGuard——turn 起始 goal 绑定表 +
  requires_host(非 auto 路由)+ stale_denial(突变工具双拼写、
  goalId 比较、goal 清空即 stale、读失败 fail-open、文案逐字节对齐 v2)。
- #7 审批 = 路由回宿主:非 auto 模式(含 mode 未知 fail-closed)下
  CreateGoal 不经原生执行,走宿主 executeTool——goal-start 审批链
  (含 mode 切换面板)原样生效,零重实现。mode 取 pipeline 快照
  (PermissionEngine::mode())。
- #8 stale veto:run_turn 入口经新 HostCallbacks::set_turn_goal 绑定
  turn→goal(默认 no-op,NativeToolCallbacks/SteerQueueCallbacks 转发,
  零装配点改动);gate 在 permission 后插入 stale_denial,denial 发
  tool.native is_error + 合成结果,不回退宿主。
- 预算宽限轮由 run_turn 硬停结构性覆盖,无需复刻(文档说明差异)。
- 顺带修复 napi callbacks.goal() 死缝:NapiHostCallbacks 增 goal_fn
  并实现(session 接线,legacy 留 None fail-open)。
- 验证:cargo lib 866(goal_guard 6 + 门级 4 + 绑定 2)、stdio 16/16
  (CreateGoal 无快照必回退 E2E)、napi-integration 49/49(manual 回退/
  auto 原生/session stale E2E)、clippy 0、oxlint 0 errors。
- ROADMAP P38 #7/#8 销账 + P42 文档(含诚实边界:mode 会话级陈旧、
  REPL 无审批、预算硬停差异);changeset 记录用户可见行为。

迁移队列:#4 P39 ✅、#3 P41 ✅、#7+#8 P42 ✅ → 剩余 #6/#2/MoonshotAI#13
7723qqq added a commit that referenced this pull request Sep 1, 2026
v2 agentExternalHooksService 的用户 PreToolUse 钩子此前对引擎原生路径
零对应(原生工具执行不触发)。本批在引擎内全量执行:

- tools/external_hooks.rs(新增):HookGuard——event 过滤(只
  PreToolUse)/matcher 正则(非法跳过)/command 去重/并行(tokio
  join_all,按序取首个 block);平台 shell spawn;stdin 写 snake_case
  载荷(hook_event_name/session_id/cwd/client_type/session_title/
  tool_name/tool_input/tool_call_id);超时 select + kill;三分支判定
  (exit 2 → stderr;stdout JSON deny → reason;其余 allow)与
  fail-closed 文案(failed to spawn / timed out / errored)逐字节对齐 v2。
- 配置随 PolicySnapshot 推送(零新 wire 字段):PolicySnapshot 增
  pre_tool_hooks;宿主 rust-engine.ts getPolicySnapshot 从
  loadRuntimeConfigSafe 读 [hooks];REPL 经 KimiConfig.hooks 段 +
  build_policy_snapshot。
- gate 集成:permission allow 后、goal_guard 前(镜像 v2 链序);
  denial 发 tool.native is_error + 合成结果,不回退宿主。
- 验证:cargo lib 879(external_hooks 11 + 门级 2)、stdio 18/18
  (exit2 拦/exit0 放行 E2E)、napi-integration 51/51(真实 .node)、
  rust-engine 25/25(宿主推送零回归)、clippy 0、oxlint 0 errors。
- ROADMAP P38 #6 销账 + P43 文档(含诚实边界:载荷字段近似、kill 链
  降级、快照会话级推送、其他 19 种事件仍归宿主、cmd 引号教训);
  changeset 记录用户可见行为。

迁移队列:#4 P39 ✅、#3 P41 ✅、#7+#8 P42 ✅、#6 P43 ✅ → 剩余
#2(toolDedupe)、MoonshotAI#13(tower worker,随 M3)。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant