kimi-code/packages/kap-server/test/debugNonloopback.e2e.test.ts
Haozhe 9b496946dc
feat(kimi-inspect): add kap-server web inspector with dev-only /api/v1/debug RPC surface (#1806)
* feat(kimi-inspect): add web inspector for kap-server /api/v2 surface

- new apps/kimi-inspect app: connect screen (server URL + optional bearer
  token, persisted in localStorage, deep-linkable via ?url=/?token=),
  workspace/session browser sidebar, per-session chat view, and live
  Service panels with data and trigger buttons for Session/Agent scopes
- built on @moonshot-ai/klient (HTTP for calls, /api/v2/ws for events);
  Vite dev server proxies /api to a running kap-server
- register the workspace in AGENTS.md project map and flake.nix
  workspacePaths/workspaceNames

* fix(kimi-inspect): align dependency versions with the workspace (sherif)

* feat: dev /api/v1/debug RPC surface and kimi-inspect channel rework

- kimi-inspect: replace @moonshot-ai/klient with an in-app old-klient-style
  channel layer (service-bound IChannel, HTTP ProxyChannel, shared /api/v2/ws
  socket with ref-counted event listens), typed by agent-core-v2 interfaces;
  /channels descriptors + serviceByName keep every wire protocol loaded 1:1
- kimi-inspect: local server auto-discovery (Vite middleware over the
  kap-server instance registry + home token), zero-config startup connect,
  and a header switcher for runtime server switching
- kap-server: wire the dormant --debug-endpoints flag to a new
  whitelist-free /api/v1/debug dispatcher (every scoped service callable),
  gated to loopback binds; repo dev scripts pass the flag
- kimi-inspect: probe the debug surface at connect, falling back to /api/v2
  on servers without it
- tests: channel + discovery unit tests in kimi-inspect; debug RPC and
  loopback-gating coverage in the kap-server rpc/debugNonloopback suites

* chore(changesets): ignore @moonshot-ai/kimi-inspect

The private dev app never ships, so it should never appear in a changeset.
Add it to the changeset config ignore list (next to vis*) and note the rule
in the gen-changesets skill.
2026-07-17 15:56:53 +08:00

101 lines
3 KiB
TypeScript

/**
* Debug-route gating (port of v1 `debug-nonloopback.e2e.test.ts`).
*
* Security property: `/api/v1/debug/*` — the dev-only, whitelist-free RPC
* surface (`--debug-endpoints`) — must NOT be reachable on a non-loopback
* bind (suppressed in `start.ts` regardless of the option), and must stay
* unmounted by default on loopback too. Pinned here: 404 on a public bind
* even with `debugEndpoints: true`, 404 on loopback without the option, and
* the mounted surface on loopback with the option.
*/
import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { type RunningServer, startServer } from '../src/start';
let prevPassword: string | undefined;
const createdDirs: string[] = [];
const running: RunningServer[] = [];
beforeEach(() => {
prevPassword = process.env['KIMI_CODE_PASSWORD'];
});
afterEach(async () => {
for (const r of running.splice(0)) {
try {
await r.close();
} catch {
// ignore
}
}
for (const dir of createdDirs.splice(0)) {
await rm(dir, { recursive: true, force: true });
}
if (prevPassword === undefined) {
delete process.env['KIMI_CODE_PASSWORD'];
} else {
process.env['KIMI_CODE_PASSWORD'] = prevPassword;
}
});
async function tmpHome(): Promise<string> {
const dir = await mkdtemp(join(tmpdir(), 'kimi-v2-debug-loopback-'));
createdDirs.push(dir);
return dir;
}
async function probeDebug(server: RunningServer): Promise<number> {
const token = server.authTokenService.getToken();
const res = await fetch(`http://127.0.0.1:${server.port}/api/v1/debug/channels`, {
headers: { authorization: `Bearer ${token}` },
});
return res.status;
}
describe('debug endpoints are not exposed on a non-loopback bind', () => {
it('returns 404 for /api/v1/debug/* on a 0.0.0.0 bind even when requested', async () => {
process.env['KIMI_CODE_PASSWORD'] = 'test-pw';
const home = await tmpHome();
const server = await startServer({
host: '0.0.0.0',
port: 0,
homeDir: home,
logLevel: 'silent',
insecureNoTls: true,
debugEndpoints: true,
});
running.push(server);
// Route suppressed → 404 (a missing route with a valid token is 404, not 401).
expect(await probeDebug(server)).toBe(404);
});
it('is not mounted on loopback by default (without the option)', async () => {
const home = await tmpHome();
const server = await startServer({
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
});
running.push(server);
expect(await probeDebug(server)).toBe(404);
});
it('mounts the whitelist-free RPC surface on loopback when requested', async () => {
const home = await tmpHome();
const server = await startServer({
host: '127.0.0.1',
port: 0,
homeDir: home,
logLevel: 'silent',
debugEndpoints: true,
});
running.push(server);
expect(await probeDebug(server)).toBe(200);
});
});