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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions server/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ export function instanceConfigs(cfg: AppConfig): InstanceConfigMap {
? cfg.instances
: {
grok: { driver: "grokAgent" },
kimi: { driver: "kimiAgent" },
claude: { driver: "claudeAgent" },
codex: { driver: "codex" },
antigravity: { driver: "antigravityAgent" },
Expand Down
58 changes: 57 additions & 1 deletion server/drivers/acp/acp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
//
// The fake CLI is a shebang script Windows cannot exec directly —
// resolveCliSpawn turns it into `node <script>`, so these run everywhere.
import { chmodSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
import { chmodSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
Expand All @@ -17,6 +17,7 @@ import type { ProviderInstance } from "../../contracts.ts";
import { recordEvents, type EventRecorder } from "../../testing/events.ts";
import { GrokAgentDriver } from "./grok.ts";
import { GeminiAgentDriver } from "./gemini.ts";
import { KimiAgentDriver } from "./kimi.ts";

const FAKE_CLI = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "testing", "fake-acp-cli.ts");

Expand All @@ -27,6 +28,15 @@ describe("ACP decodeConfig", () => {
it("gemini defaults to the gemini binary", () => {
expect(GeminiAgentDriver.decodeConfig(undefined)).toEqual({ cli: "gemini", fullAuto: false, workspace: undefined });
});
it("kimi defaults to the kimi binary and declares cross-platform setup", () => {
expect(KimiAgentDriver.decodeConfig(undefined)).toEqual({ cli: "kimi", fullAuto: false, workspace: undefined });
expect(KimiAgentDriver.install?.command).toMatchObject({
darwin: expect.stringContaining("install.sh"),
linux: expect.stringContaining("install.sh"),
win32: expect.stringContaining("install.ps1"),
});
expect(KimiAgentDriver.install?.signInCommand).toBe("kimi login");
});
it("fullAuto only when explicitly true", () => {
expect(GrokAgentDriver.decodeConfig({ fullAuto: "yes" }).fullAuto).toBe(false);
expect(GrokAgentDriver.decodeConfig({ fullAuto: true }).fullAuto).toBe(true);
Expand Down Expand Up @@ -177,4 +187,50 @@ describe("ACP snapshot", () => {
expect(snap.state).toBe("unavailable");
await instance.dispose();
});

it("kimi checks KIMI_CODE_HOME before the child HOME", async () => {
const scratch = mkdtempSync(join(tmpdir(), "omb-kimi-auth-"));
const kimiHome = join(scratch, "custom-kimi-home");
const childHome = join(scratch, "child-home");
mkdirSync(join(childHome, ".kimi-code", "credentials"), { recursive: true });
writeFileSync(join(childHome, ".kimi-code", "credentials", "kimi-code.json"), "{}");

const instance = await KimiAgentDriver.create({
instanceId: "kimi-custom-home",
displayName: undefined,
environment: { KIMI_CODE_HOME: kimiHome, HOME: childHome },
enabled: true,
config: { cli: FAKE_CLI, fullAuto: false },
});
try {
expect((await instance.snapshot()).authenticated).toBe(false);
mkdirSync(join(kimiHome, "credentials"), { recursive: true });
writeFileSync(join(kimiHome, "credentials", "kimi-code.json"), "{}");
expect((await instance.snapshot()).authenticated).toBe(true);
} finally {
await instance.dispose();
rmSync(scratch, { recursive: true, force: true });
}
});

it("kimi resolves default credentials from the child HOME", async () => {
const scratch = mkdtempSync(join(tmpdir(), "omb-kimi-home-"));
const credentialDir = join(scratch, ".kimi-code", "credentials");
mkdirSync(credentialDir, { recursive: true });
writeFileSync(join(credentialDir, "kimi-code.json"), "{}");

const instance = await KimiAgentDriver.create({
instanceId: "kimi-child-home",
displayName: undefined,
environment: { HOME: scratch },
enabled: true,
config: { cli: FAKE_CLI, fullAuto: false },
});
try {
expect((await instance.snapshot()).authenticated).toBe(true);
} finally {
await instance.dispose();
rmSync(scratch, { recursive: true, force: true });
}
});
});
74 changes: 74 additions & 0 deletions server/drivers/acp/kimi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Kimi Code harness support — Moonshot's `kimi` CLI over ACP stdio
// (`kimi acp`), on the Kimi Code subscription login
// (~/.kimi-code/credentials/kimi-code.json), not a Moonshot API key.
// The generic protocol runtime lives in acp/core.ts; this file is only the
// per-harness quirks. Verified against kimi-code 0.29.1: initialize reports
// loadSession:true (session/load resume works), mcpCapabilities http+sse,
// and a full session/new → session/prompt roundtrip streams
// agent_thought_chunk + agent_message_chunk and settles with end_turn.
import { existsSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";

import { createAcpDriver, type AcpSupport } from "./core.ts";

function credentialsPath(env: Record<string, string | undefined>) {
const dataRoot = env.KIMI_CODE_HOME || join(env.HOME || homedir(), ".kimi-code");
return join(dataRoot, "credentials", "kimi-code.json");
}

const support: AcpSupport = {
driverKind: "kimiAgent",
displayName: "Kimi",
// Aliases from the CLI's own catalog (~/.kimi-code/config.toml
// [models."kimi-code/…"] — `kimi provider list` reports the same four).
models: {
default: "kimi-code/k3",
options: [
{ id: "kimi-code/k3", label: "Kimi K3" },
{ id: "kimi-code/k3-256k", label: "Kimi K3 256K" },
{ id: "kimi-code/kimi-for-coding", label: "Kimi for Coding" },
{ id: "kimi-code/kimi-for-coding-highspeed", label: "Kimi for Coding Highspeed" },
],
},
defaultCli: "kimi",
nativeSource: "kimi.acp",
loginNote: "Kimi Code CLI is not signed in — run `kimi login` in a terminal",

// Official installers put the binary on PATH without requiring an existing
// Node install. Keep the commands platform-specific so Windows never gets a
// POSIX-only curl|bash instruction.
install: {
command: {
darwin: "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash",
linux: "curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash",
win32: "irm https://code.kimi.com/kimi-code/install.ps1 | iex",
},
docsUrl: "https://moonshotai.github.io/kimi-code/en/guides/getting-started.html",
signInCommand: "kimi login",
},

// -m is a global commander option and must precede the `acp` subcommand
// (verified against 0.29.1).
spawnArgs: (_config, turn) => [...(turn.model ? ["-m", turn.model] : []), "acp"],

// Subscription CLI: a leaked Moonshot/Kimi API key must not flip billing
// to pay-as-you-go inside the spawned agent (mirrors claude/grok).
transformEnv: (env) => {
delete env.MOONSHOT_API_KEY;
delete env.KIMI_API_KEY;
},

// The only advertised authMethod is {id:"login", type:"terminal"} — a
// device-code flow that cannot be driven over ACP. Never pick it; ride
// the ambient login from a prior `kimi login` instead.
pickAuthMethod: () => null,
authFailure: "continue",
// Match the child CLI's own data-root precedence. A custom instance HOME or
// KIMI_CODE_HOME must not be checked against the server user's home instead.
isAuthenticated: (env) => existsSync(credentialsPath(env)),

buildPromptText: (turn) => (turn.system ? `${turn.system}\n\n${turn.text}` : turn.text),
};

export const KimiAgentDriver = createAcpDriver(support);
2 changes: 2 additions & 0 deletions server/drivers/builtIn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import { CodexDriver } from "./codex.ts";
import { GrokDriver } from "./grok.ts";
import { GrokAgentDriver } from "./acp/grok.ts";
import { GeminiAgentDriver } from "./acp/gemini.ts";
import { KimiAgentDriver } from "./acp/kimi.ts";

export const BUILT_IN_DRIVERS: readonly AnyProviderDriver[] = [
GrokDriver,
GrokAgentDriver,
GeminiAgentDriver,
KimiAgentDriver,
ClaudeDriver,
CodexDriver,
AntigravityDriver,
Expand Down
Loading