diff --git a/CHANGELOG.md b/CHANGELOG.md index 25c1c6234..d9a2190f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 未发布 + +- 远程 Claude Docker 槽改用镜像烘焙 kernel 入口(`/opt/kin/kin-kernel`,`REMOTE_KERNEL_ENTRY`),不改 GPT 与本机/KVM 命令;新建或重建容器后生效。(#489、#496) + ## 1.3.181 — 2026-10-17 - 采集客户机 CPU、内存、磁盘、MAC、DMI、Shell 与工具链,写入槽位特征并展示于运维页;终端启动打印实时系统特征。 diff --git a/src/lib/cluster/remote-slot.mjs b/src/lib/cluster/remote-slot.mjs index 057ce42ae..e96579e98 100644 --- a/src/lib/cluster/remote-slot.mjs +++ b/src/lib/cluster/remote-slot.mjs @@ -28,7 +28,7 @@ import { proxyKey, } from '../vm/egress.mjs' import { OS_CATALOG } from '../vm/os-catalog.mjs' -import { resolveKernelDataplane } from '../vm/slot-engine.mjs' +import { REMOTE_KERNEL_ENTRY, resolveKernelDataplane } from '../vm/slot-engine.mjs' import { isCodexVm } from '../vm/vm-kind.mjs' import { proxyBlockedReason } from '../vm/proxy-policy.mjs' import { isKvmRuntime, runtimeKind } from '../vm/runtime-kind.mjs' @@ -42,7 +42,6 @@ import { normalizeTimezone, KVM_CAPS, STANDARD_LOCALE, - slotContainerCommand, writeWorkerFiles, } from '../vm/vm-runtime.mjs' import { @@ -489,7 +488,8 @@ export function slotContainerBody(vm, { image, network, remoteDir, user, routing `KIN_VM_NAME=${slotName}`, `KIN_VM_OS=${vm.kernel}`, ], - Cmd: slotContainerCommand(vm), + // Node images bake the Claude kernel here; local slots bind-mount a different path. + Cmd: codex ? ['sleep', 'infinity'] : [REMOTE_KERNEL_ENTRY, '--gateway-worker', '--config', '/run/kin/kernel.json'], Labels: { 'kin.vm': '1', 'kin.vm.id': vm.id, diff --git a/test/unit/cluster-placement.test.mjs b/test/unit/cluster-placement.test.mjs index f364ea8b0..1d66ba628 100644 --- a/test/unit/cluster-placement.test.mjs +++ b/test/unit/cluster-placement.test.mjs @@ -16,7 +16,7 @@ import { startSlotImageBuild, unbindPlacement, } from '../../src/lib/cluster/placement.mjs' -import { remoteIptablesScript, slotEgressNames } from '../../src/lib/cluster/remote-slot.mjs' +import { remoteIptablesScript, slotContainerBody, slotEgressNames } from '../../src/lib/cluster/remote-slot.mjs' import { pullSlotCredentials, pushSlotCredentials, @@ -283,6 +283,14 @@ test('slot image tag follows payload bytes only; a VERSION bump keeps the image' fs.chmodSync(process.env.KIN_CODEX_KERNEL_BIN, 0o755) const a = slotImageSpec(root, 'ubuntu-24.04') + const container = slotContainerBody( + { id: 'vm-03', platform: 'anthropic', node_id: 'node-a', kernel: 'ubuntu-24.04' }, + { image: a.ref, network: 'test-net', remoteDir: '/home/node/vms/vm-03', user: '1000:1000', routing: {} }, + ) + const entry = a.files.find((file) => `/${file.name}` === container.Cmd[0]) + assert.ok(entry, 'the remote Claude command must name a file baked into its slot image') + assert.equal(entry.mode, 0o755) + assert.match(entry.body.toString(), /^#!\/bin\/sh/) assert.match(a.ref, /^vm2api\/kin-slot-ubuntu-24\.04:[0-9a-f]{12}$/) const kvm = slotImageSpec(root, 'ubuntu-24.04', { runtime: 'kvm' }) assert.match(kvm.ref, /^vm2api\/kin-slot-kvm-ubuntu-24\.04:[0-9a-f]{12}$/) diff --git a/test/unit/remote-slot-gpt.test.mjs b/test/unit/remote-slot-gpt.test.mjs index 8be25a253..08912e9ed 100644 --- a/test/unit/remote-slot-gpt.test.mjs +++ b/test/unit/remote-slot-gpt.test.mjs @@ -2,7 +2,8 @@ import test from 'node:test' import assert from 'node:assert/strict' import { SLOT_CAPS_NODE } from '../../src/lib/cluster/placement.mjs' import { codexRoleMatches, slotContainerBody } from '../../src/lib/cluster/remote-slot.mjs' -import { CODEX_SLOT_ROLE } from '../../src/lib/vm/vm-runtime.mjs' +import { REMOTE_KERNEL_ENTRY } from '../../src/lib/vm/slot-engine.mjs' +import { CODEX_SLOT_ROLE, slotContainerCommand } from '../../src/lib/vm/vm-runtime.mjs' import { slotHost } from '../../src/lib/vm/slot-host.mjs' import { startSlotReady } from '../../src/lib/vm/slot-runtime.mjs' @@ -30,14 +31,40 @@ test('a remote GPT container idles instead of booting the Claude kernel', () => const spec = body(gpt) assert.deepEqual(spec.Cmd, ['sleep', 'infinity']) assert.equal(spec.Labels['kin.vm.role'], CODEX_SLOT_ROLE) - const claudeSpec = body(claude) - assert.deepEqual(claudeSpec.Cmd, [ +}) + +for (const runtime of [undefined, { type: 'docker' }]) { + test(`a remote Claude slot uses the baked kernel entry (${runtime?.type || 'default'} runtime)`, () => { + const spec = body({ ...claude, runtime }) + assert.equal(REMOTE_KERNEL_ENTRY, '/opt/kin/kin-kernel') + assert.deepEqual(spec.Cmd, [REMOTE_KERNEL_ENTRY, '--gateway-worker', '--config', '/run/kin/kernel.json']) + assert.equal(spec.Labels['kin.vm.role'], undefined) + assert.equal(spec.Labels['kin.vm.runtime'], 'docker') + }) +} + +test('local Claude commands retain their bind-mounted and wrapped kernel entries', () => { + assert.deepEqual(slotContainerCommand(claude), [ '/usr/local/bin/kin-kernel', '--gateway-worker', '--config', '/run/kin/kernel.json', ]) - assert.equal(claudeSpec.Labels['kin.vm.role'], undefined) + assert.deepEqual(slotContainerCommand(claude, { wrapKernel: true }), [ + '/home/kincli/.kin/kin-kernel', + '--gateway-worker', + '--config', + '/run/kin/kernel.json', + ]) + assert.deepEqual(slotContainerCommand(gpt), ['sleep', 'infinity']) +}) + +test('remote KVM slots retain the runner image command for both platforms', () => { + for (const vm of [claude, gpt]) { + const spec = body({ ...vm, runtime: { type: 'kvm' } }) + assert.equal(spec.Labels['kin.vm.runtime'], 'kvm') + assert.equal(Object.hasOwn(spec, 'Cmd'), false) + } }) test('a remote GPT container still running kin-kernel is recreated', () => {