kimi-code/flake.nix
Haozhe 1328b32037
feat(acp): add experimental agent-core-v2 ACP server (kimi acp-v2) (#2571)
* feat(acp): add agent-core-v2 ACP server

- add ACP session lifecycle, configuration, permissions, and event bridging
- expose the experimental kimi acp-v2 command with terminal authentication
- add integration coverage and workspace build configuration

* test: use neutral example domains in test fixtures and docs

- replace placeholder hostnames (evil.com, foo.com, internal.corp,
  real.corp) with example.test / example.com in agent-core-v2 and
  kap-server tests
- replace fixture emails (x@y.com, a@x.com) with example addresses in
  minidb tests and README

* fix(acp): align acp-server with agent-core-v2 interfaces and address review

- add missing appendText to AcpHostFileSystem (IHostFileSystem drift)
- replace IAgentPromptService.prompt with inject
- use Turn.cancel() instead of abortController
- gate FS reverse-RPCs on client capabilities, fallback to local FS
- return PROTOCOL_VERSION constant instead of echoing client version
- remove misleading mcpCapabilities from initialize response
- dispose old session wrapper before replacing on load/resume
- fix object stringification lint error in convert.ts
- add acp-v2 to expected CLI sub-command list in test

* fix(acp): use enqueue for prompt submission, stop advertising unimplemented builtins

- replace IAgentPromptService.inject with enqueue so onBeforeSubmitPrompt
  hooks (prompt-blocking policy) are not bypassed
- stop advertising builtin slash commands (/help, /status, etc.) until
  builtin command execution is implemented
- add comment explaining appendText stays local (ACP has no append RPC)
- update skills test to match new availableCommands behavior

* fix(acp): filter turn events by turnId, surface auth failures as auth_required

- track turnId in driveTurn and ignore events from unrelated turns,
  preventing queued prompts from settling on the running turn
- reject prompt requests with auth_required when turn fails with an
  auth-related error code, enabling ACP client re-auth flow

* fix(acp): gate acp-v2 behind experimental flag, filter sessions by cwd

- add acp-v2 experimental flag (KIMI_CODE_EXPERIMENTAL_ACP_V2) and gate
  CLI command registration behind it
- filter session/list results by requested cwd instead of returning
  sessions from all workspaces
- detect hook-blocked prompts via PromptHandle.state and add TODO for
  streaming block messages once the hook context exposes them

* refactor(acp-server): rewire ACP server onto the klient facade

- replace direct agent-core-v2 scope/service access (ISessionLifecycleService,
  ISessionIndex, IEventBus, ISessionInteractionService, etc.) with the Klient
  facade: klient.global.sessions / klient.session(id) / agent('main') handles
- drive turns via agent.prompt() + session-level agent event subscriptions
  instead of per-prompt IEventBus wiring; settle on turn.ended
- route approval/question bridging through session.interactions events
- hide the thinking config option and skill catalog behind KLIENT-GAP markers
  until klient exposes those surfaces
- acp-fs: pass realpath through to the local inner backend
- klient: session.restore() rejects both null and undefined handles

* feat(agent-core-v2): add session delete and ephemeral per-session MCP servers

- add ISessionLifecycleService.delete: close a live session first, then
  remove its persisted data, evict the index read-model entry, and append
  a deleted tombstone to session_index.jsonl; unknown ids raise
  session.not_found
- add CreateSessionOptions/ResumeSessionOptions.mcpServers: session-owned
  MCP overlay merged over the workspace manager via
  MergedMcpConnectionView (an ephemeral name shadows a workspace server),
  never persisted, released when the session scope tears down
- return PromptLaunchResult from activateSkill so callers get the
  launched turn id and activation failures (unknown skill, busy) surface
- add ISessionSkillCatalog.list() as a wire-friendly catalog snapshot
- add ISessionIndex.remove for read-model eviction on delete

* feat(klient): expose session delete, per-session MCP, skills, and stream events

- session lifecycle contract: delete, resume/restore options, and
  CreateSessionOptions.mcpServers (ephemeral per-session MCP servers)
- add the session skills contract and facade accessors for the
  wire-friendly skill catalog snapshot
- register tool.call.delta, tool.progress, and compaction.* agent stream
  events so consumers can subscribe with typed payloads

* feat(acp-server): align ACP v2 server with acp-adapter capabilities

- complete the klient-facade rewire: ACP client connection holder and
  the terminal/* reverse-RPC runner routed through the Agent scope
- negotiate the protocol version on initialize instead of pinning v1
- compress oversized prompt images at the ACP ingestion point with a
  format gate, caption, and persisted originals; a cancel arriving
  mid-compression settles the prompt as cancelled without a turn
- stream tool call args via tool.call.delta (lazy pending create,
  cumulative replace, started upgrade) and refresh titles via
  tool.progress status updates
- report compaction progress and results after /compact via the
  compaction.* events
- answer unknown slash commands locally instead of sending them to the
  model
- accept legacy "<id>,thinking" model ids and legacy approve /
  approve_for_session approval option ids
- keep sessions without cwd metadata in cwd-filtered session/list
- sanitize wire errors: auth codes map to auth_required, turn.agent_busy
  to invalid_request, everything else to a fixed internal-error message
- bump @agentclientprotocol/sdk to ^1.3.0

* fix(cli): drop stale registerServerCommand call and sherif ACP SDK split

- commands.ts called registerServerCommand, which no longer exists on
  current main (the deprecated `kimi server` shim is registered via
  registerWebCommand), breaking typecheck, build, and every CLI test
  that builds the program
- sherif rejects the @agentclientprotocol/sdk major split between
  acp-adapter (^0.23.0, production kimi acp) and acp-server (^1.3.0,
  experimental); the two hosts legitimately target different SDK
  majors, so ignore the dependency in the sherif invocation

* test: update fixtures for acp-v2 flag and domain rename, refresh nix deps hash

- kap-server origin.test: two CORS cases still used foo.com after the
  whitelist moved to foo.example.com, so the origin was no longer
  whitelisted and the expected CORS headers were withheld
- node-sdk config.test: expect the new acp-v2 experimental flag in the
  harness feature metadata
- flake.nix: update the fetchPnpmDeps hash for the
  @agentclientprotocol/sdk 1.3.0 lockfile change

* fix(acp): widen the ACP v2 auth gate beyond OAuth-only providers

The gate consulted only auth.summarize(), which iterates providers
declaring an oauth section — configurations that authenticate with a
plain apiKey or provider env-bag credentials (no OAuth at all) were
rejected with auth_required even though the default model is fully
usable.

- klient: expose authSummaryService.ensureReady on the global auth
  facade (the contract already declared it)
- acp-server: gate on the engine's own readiness probe for the default
  model — config apiKey / env-bag / OAuth token all count, matching how
  the model is actually used — and fall back to "any logged-in OAuth
  provider" (the legacy adapter's first branch)
- test: an apiKey-only config passes the gate with auth enforcement on;
  the OAuth logout regression is unchanged

* fix(acp): reject concurrent prompts instead of displacing the in-flight turn

A second session/prompt while a turn is running overwrote the session's
only TurnDriver: the engine quietly queues plain prompts submitted
during an active turn (the launch resolves undefined, indistinguishable
from a hook-blocked launch), so the first prompt never settled and both
turns' events went unattributed.

Guard both model-bound launch paths (plain prompt and skill activation)
with a synchronous in-flight check and reject with invalid_request
(turn.agent_busy), matching the legacy adapter's busy semantics. Local
slash handling (builtins, unknown-command answers) is unaffected.
2026-08-04 10:20:24 +08:00

267 lines
8.6 KiB
Nix

{
description = "Kimi Code CLI";
inputs = {
# Pinned to the 25.11 release channel because nixpkgs-unstable currently
# ships nodejs_24 = 24.14.1, which trips the >= 24.15.0 floor that the
# native SEA build enforces (see apps/kimi-code/scripts/native/build.mjs).
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
};
outputs =
{ self, nixpkgs }:
let
lib = nixpkgs.lib;
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
forAllSystems =
f:
lib.genAttrs systems (
system:
f (import nixpkgs {
inherit system;
})
);
minNodeVersion = "24.15.0";
# Hardcode to Node.js 24.x; fail the evaluation if the pinned nixpkgs
# does not offer a new enough 24.x.
nodejsFor =
pkgs:
let
node = pkgs.nodejs_24;
in
if lib.versionAtLeast node.version minNodeVersion then
node
else
throw ''
Kimi Code requires Node.js >= ${minNodeVersion},
but nixpkgs only offers ${node.version}.
Pin a newer nixpkgs revision or update minNodeVersion in flake.nix.
'';
pnpmFor =
pkgs:
pkgs.pnpm_10.override {
nodejs = nodejsFor pkgs;
};
# -------------------------------------------------------------------
# Workspace members (kept in sync with pnpm-workspace.yaml).
#
# HARD REQUIREMENT: whenever you add or remove a workspace package,
# you MUST update both lists below. Missing a path will break the Nix
# build (src fileset silently drops files); missing a name will break
# pnpmConfigHook (dependencies for that workspace won't be fetched).
# -------------------------------------------------------------------
workspacePaths = [
./packages/acp-adapter
./packages/acp-server
./packages/agent-core
./packages/agent-core-v2
./packages/kap-server
./packages/kaos
./packages/klient
./packages/kosong
./packages/migration-legacy
./packages/minidb
./packages/node-sdk
./packages/oauth
./packages/pi-tui
./packages/protocol
./packages/telemetry
./packages/transcript
./packages/tree-sitter-bash
./apps/kimi-code
./apps/vscode
./apps/kimi-inspect
./apps/kimi-web
./apps/vis
./apps/vis/server
./apps/vis/web
./docs
];
workspaceNames = [
"@moonshot-ai/acp-adapter"
"@moonshot-ai/acp-server"
"@moonshot-ai/agent-core"
"@moonshot-ai/agent-core-v2"
"@moonshot-ai/kap-server"
"@moonshot-ai/kaos"
"@moonshot-ai/kosong"
"@moonshot-ai/migration-legacy"
"@moonshot-ai/minidb"
"@moonshot-ai/kimi-code-sdk"
"@moonshot-ai/kimi-code-oauth"
"@moonshot-ai/klient"
"@moonshot-ai/pi-tui"
"@moonshot-ai/protocol"
"@moonshot-ai/kimi-telemetry"
"@moonshot-ai/transcript"
"@moonshot-ai/tree-sitter-bash"
"@moonshot-ai/kimi-code"
"kimi-code"
"@moonshot-ai/kimi-inspect"
"@moonshot-ai/kimi-web"
"@moonshot-ai/vis"
"@moonshot-ai/vis-server"
"@moonshot-ai/vis-web"
"kimi-code-docs"
];
in
{
packages = forAllSystems (
pkgs:
let
nodejs = nodejsFor pkgs;
pnpm = pnpmFor pkgs;
appPackageJson = builtins.fromJSON (builtins.readFile ./apps/kimi-code/package.json);
nativeTarget =
if pkgs.stdenv.hostPlatform.isLinux && pkgs.stdenv.hostPlatform.isAarch64 then
"linux-arm64"
else if pkgs.stdenv.hostPlatform.isLinux then
"linux-x64"
else if pkgs.stdenv.hostPlatform.isDarwin && pkgs.stdenv.hostPlatform.isAarch64 then
"darwin-arm64"
else if pkgs.stdenv.hostPlatform.isDarwin then
"darwin-x64"
else
throw "Unsupported Kimi Code native target for ${pkgs.stdenv.hostPlatform.system}";
kimi-code = pkgs.stdenv.mkDerivation (finalAttrs: {
pname = "kimi-code";
version = appPackageJson.version;
src = lib.fileset.toSource {
root = ./.;
fileset = lib.fileset.unions (
[
./build
./.npmrc
./.nvmrc
./package.json
./pnpm-lock.yaml
./pnpm-workspace.yaml
./tsconfig.json
./vitest.config.ts
./LICENSE
]
++ workspacePaths
);
};
pnpmWorkspaces = [ "." ] ++ workspaceNames;
pnpmDeps = pkgs.fetchPnpmDeps {
inherit (finalAttrs) pname version src pnpmWorkspaces;
inherit pnpm;
fetcherVersion = 3;
hash = "sha256-dRVe0stYIorD8H/SyAIAJof6JinpT/3E/I0lyllbFWg=";
};
nativeBuildInputs = [
nodejs
pnpm
(pkgs.pnpmConfigHook.override { inherit pnpm; })
pkgs.makeWrapper
]
# The SEA inject step (postject) invalidates the macOS code
# signature on the copied Node executable; build.mjs then re-applies
# an ad-hoc signature via `codesign`. The Nix darwin sandbox does
# not expose /usr/bin/codesign, so we supply nixpkgs' ad-hoc-only
# replacement instead.
++ lib.optionals pkgs.stdenv.hostPlatform.isDarwin [
pkgs.darwin.sigtool
];
# The SEA binary is produced by `postject`-injecting a blob into a
# plain Node executable. Stripping rewrites section tables and can
# invalidate the injected blob's offsets, so leave the binary
# untouched after the build.
dontStrip = true;
buildPhase = ''
runHook preBuild
export KIMI_CODE_BUILD_TARGET=${nativeTarget}
${lib.optionalString pkgs.stdenv.hostPlatform.isDarwin ''
# pkgs.darwin.sigtool's codesign supports `--sign -` (ad-hoc)
# but not the inspection mode (`-dv`) that 05-verify.mjs runs
# afterwards. Disable the verify step for the Nix build; the
# release CI keeps it via the unmodified script.
substituteInPlace apps/kimi-code/scripts/native/build.mjs \
--replace-fail \
"await runVerifyStep({ requireGatekeeper: false });" \
"// runVerifyStep skipped in nix sandbox (sigtool lacks -dv)"
''}
# The SEA blob step (scripts/native/02-sea-blob.mjs) embeds the
# Kimi web assets from apps/kimi-code/dist-web and fails if that
# directory is missing. Build the web app and stage its assets
# before producing the native executable.
pnpm --filter=@moonshot-ai/kimi-web run build
node apps/kimi-code/scripts/copy-web-assets.mjs
pnpm --filter=@moonshot-ai/kimi-code run build:native:sea
runHook postBuild
'';
installPhase = ''
runHook preInstall
install -Dm755 \
"apps/kimi-code/dist-native/bin/${nativeTarget}/kimi" \
"$out/bin/kimi"
runHook postInstall
'';
postInstall = ''
wrapProgram $out/bin/kimi --prefix PATH : ${lib.makeBinPath [ pkgs.ripgrep pkgs.fd ]}
'';
meta = {
description = "Kimi Code CLI";
homepage = "https://github.com/MoonshotAI/kimi-code";
license = lib.licenses.mit;
mainProgram = "kimi";
platforms = systems;
};
});
in
{
inherit kimi-code;
default = kimi-code;
}
);
apps = forAllSystems (pkgs: {
kimi-code = {
type = "app";
program = "${self.packages.${pkgs.system}.kimi-code}/bin/kimi";
};
default = self.apps.${pkgs.system}.kimi-code;
});
devShells = forAllSystems (pkgs: {
default =
let
nodejs = nodejsFor pkgs;
pnpm = pnpmFor pkgs;
in
pkgs.mkShell {
packages = [
nodejs
pnpm
pkgs.ripgrep
pkgs.fd
];
};
});
};
}