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 CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 未发布

- 槽位种子页默认锁定:需确认解锁后才能改配置,播种前再次确认;时区/语言提供 en、cn、jp 基础选项。时区跟随当前 SOCKS5(空/非手动钉住)时展示出口时区,播种经 `timezone_follow_proxy` 写入;可改固定 IANA 时区或改回跟随。TZ/LANG/LC_ALL 仍不经 `extra_env` 覆盖契约键。
- 槽位种子页:`settings.json` 的 env 行与 `theme` 可直接编辑并随播种写入;`autoUpdates`、`grove_enabled` 只读且播种时固定为 `false`;TZ/LANG/LC_ALL、遥测 kill-switch、Bedrock/Vertex、gateway hint 等同理只读(播种由契约重写)。缺 `CLAUDE_CODE_ATTRIBUTION_HEADER` 时默认 `0`。调试上传默认关,独立 query 拉最近 40 条上传,切换开关不重置未保存编辑;Go sidecar 写 `vms/<id>/run/telemetry-upload.log`,面板 `GET seed-settings?debug=1` 返回 `telemetry_log`。
- 槽位种子页按当前槽 `settings.json` 列出真实配置;`claude.setting` 可编辑播种;缺失文件返回 `null`、空文件返回空串,不用策略默认冒充槽内文件;遥测开关与 sidecar 进程状态分开展示,保存不因遥测进程刷新失败而 502。
- fleet 遥测刷新在 kin-egress 二进制被替换(含 `(deleted)` 旧进程与脏 pidfile)时,会按 egress 配置扫停全部 helper、等待 `/proc/<pid>` 消失后再绑定新二进制;同一 SOCKS 出口的 stop/start 与 `ensureProxyEgress` 共用 per-proxy 锁,避免并发 telemetry 互相 SIGTERM。(#604)
Expand Down
2 changes: 1 addition & 1 deletion docs/PANEL_API.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
| POST | `/vms/:id/oauth/generate-auth-url` | PKCE 授权链接;无 SOCKS5 拒绝。`{ flavor: "claude_code" }` 为官方 Claude Code 授权页。`{ flavor: "setup_token" }` 为 inference-only PKCE,不启槽内 CLI |
| POST | `/vms/:id/oauth/exchange-code` | 粘贴授权码,经槽代理换票。完整 OAuth 才排队初装。flavor 以 session 为准 |
| GET/POST | `/vms/:id/official-cc-bootstrap` | 初装进度 / `{ manual:true }` 再跑 |
| GET/PUT | `/vms/:id/seed-settings` | 播种;`settings.json` env 与 `theme` 经 PUT `extra_env` / `theme` 可编辑;`autoUpdates`/`grove_enabled` 播种固定;强制保留 telemetry/bedrock/vertex 等 env;`claude_setting` 可编辑并写入槽内 `.claude/claude.setting`。`GET ?debug=1` 额外返回 `telemetry_log`(最近 40 行脱敏上传审计,默认不带) |
| GET/PUT | `/vms/:id/seed-settings` | 播种;面板默认锁定,解锁后改、播种前确认。`settings.json` 可编辑 env(经 `extra_env`)与 `theme`;`autoUpdates`/`grove_enabled` 与 TZ/LANG/LC_ALL、telemetry/bedrock/vertex 等契约 env 只读展示,TZ/LANG 经 PUT `timezone` / `timezone_follow_proxy` / `locale` 写入槽位后再播种,不经 `extra_env`;`claude_setting` 可编辑。`GET ?debug=1` 额外返回 `telemetry_log`(最近 40 行脱敏上传审计,默认不带) |
| POST | `/vms/:id/collect-identity` | guest 采集(locale/tz/`guest_machine_id`) |
| POST | `/vms/:id/reload` | 重载该槽 worker |
| GET | `/wrap-cli` | kernel / wrap 样本 inspect:`ok, dir, kernel_bin, glibc_shim, wrapper, meta, kernel`。`kernel.source` 为 `configured`(仓内 `KIN_KERNEL_BIN` / `bin/kin-kernel`)或 `sample` |
Expand Down
51 changes: 51 additions & 0 deletions src/lib/admin/panel-routes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3575,6 +3575,57 @@ export function createPanelHandler(ctx) {
const next = defaultSeedPolicy(merged)
if (body.settings_json_override !== undefined) next.settings_json_override = body.settings_json_override
if (body.extra_env !== undefined) next.extra_env = body.extra_env || {}
const followProxyTz = body?.timezone_follow_proxy === true || body?.timezoneFollowProxy === true
const hasTimezone = body && Object.prototype.hasOwnProperty.call(body, 'timezone')
const hasLocale = body && Object.prototype.hasOwnProperty.call(body, 'locale')
let nextLocale = null
if (hasLocale) {
const requestedLocale = body.locale == null || body.locale === '' ? STANDARD_LOCALE : String(body.locale)
if (!SLOT_LOCALES.includes(requestedLocale)) {
return json(res, 400, {
ok: false,
error: { code: 'invalid_locale', message: `语言必须是 ${SLOT_LOCALES.join('、')}` },
})
}
nextLocale = requestedLocale
}
if (hasTimezone) {
const zone = validTimezone(body.timezone)
if (!zone) {
return json(res, 400, {
ok: false,
error: {
type: 'invalid_request_error',
code: 'invalid_timezone',
message: '时区必须是有效的 IANA 名称,如 Asia/Tokyo',
param: 'timezone',
},
})
}
const pinned = persistVmTimezone(cfg.paths.project, id, zone, { source: 'manual' })
if (!pinned) return json(res, 404, { ok: false, error: { message: 'vm not found' } })
Object.assign(vm, pinned)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seed pin overwrites live VM record

High Severity

Pinning a timezone on seed PUT assigns persistVmTimezone's summarizeVm result onto the full slot record and writes that back. The summary redacts proxy.password to *** and injects panel-only fields, so a seed that pins TZ can wipe the SOCKS password and pollute vms/<id>.json.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6e055dc. Configure here.

} else if (followProxyTz) {
const synced = await syncVmTimezoneFromProxy(cfg.paths.project, proxyPool, id, { force: true })
if (!synced.ok) {
return json(res, 400, {
ok: false,
error: {
type: 'invalid_request_error',
code: synced.reason || 'proxy_timezone_unknown',
message: '未能从代理节点取到时区,请先检测代理地理位置',
},
})
}
if (synced.applied) {
const fresh = getVm(cfg.paths.project, id)
if (fresh) Object.assign(vm, fresh)
} else if (synced.timezone) {
vm.timezone = synced.timezone
vm.timezone_source = 'proxy_geo'
}
}
if (nextLocale) vm.locale = nextLocale

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Locale seed skips fingerprint locale

Medium Severity

Seed PUT writes vm.locale only. Timezone persist also copies the zone onto fingerprint.timezone, but locale does not update fingerprint.locale. Outbound identity still prefers the fingerprint, so LANG in seed files can disagree with workstation locale until a fingerprint reset.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6e055dc. Configure here.

vm.seed_policy = next
vm.updated_at = new Date().toISOString()
atomicWriteJson(vmPath, vm, { mode: 0o600 })
Expand Down
56 changes: 53 additions & 3 deletions test/unit/panel-vm-seed-settings.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ function seed(root, id) {
)
}

function handler(project) {
function handler(project, { readBody = async () => ({}) } = {}) {
const response = {}
const handlePanel = createPanelHandler({
cfg: { paths: { project }, vm: { id: null } },
Expand All @@ -34,8 +34,19 @@ function handler(project) {
response.body = payload
return true
},
readBody: async () => ({}),
proxyPool: { unbindVm() {} },
readBody,
proxyPool: {
unbindVm() {},
followProxyTimezoneEnabled() {
return true
},
proxyTimezone() {
return 'Asia/Shanghai'
},
timezoneForVm() {
return 'Asia/Shanghai'
},
},
})
return { handlePanel, response }
}
Expand Down Expand Up @@ -74,6 +85,45 @@ test('GET seed-settings returns an empty claude.setting file as empty', async ()
}
})

test('PUT seed-settings can follow proxy timezone and locale before writing files', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kin-seed-put-tz-'))
try {
seed(root, 'vm-01')
const vmPath = path.join(root, 'vms', 'vm-01.json')
const vm = JSON.parse(fs.readFileSync(vmPath, 'utf8'))
vm.timezone = 'UTC'
vm.timezone_source = 'manual'
vm.locale = 'en_US.UTF-8'
vm.proxy = { id: 'px-1' }
fs.writeFileSync(vmPath, JSON.stringify(vm))
const body = {
telemetry_disabled: true,
claude_setting: 'X=1',
extra_env: { CUSTOM: 'yes' },
theme: 'dark',
timezone_follow_proxy: true,
locale: 'zh_CN.UTF-8',
}
const { handlePanel, response } = handler(root, {
readBody: async () => body,
})
await handlePanel(
{ method: 'PUT', url: '/api/panel/vms/vm-01/seed-settings' },
{},
new URL('http://localhost/api/panel/vms/vm-01/seed-settings'),
)
assert.equal(response.status, 200, JSON.stringify(response.body).slice(0, 400))
const saved = JSON.parse(fs.readFileSync(vmPath, 'utf8'))
assert.equal(saved.locale, 'zh_CN.UTF-8')
assert.equal(saved.timezone, 'Asia/Shanghai')
assert.equal(response.body.data.settings_json.env.TZ, 'Asia/Shanghai')
assert.equal(response.body.data.settings_json.env.LANG, 'zh_CN.UTF-8')
assert.equal(response.body.data.settings_json.env.CUSTOM, 'yes')
} finally {
fs.rmSync(root, { recursive: true, force: true })
}
})

test('GET seed-settings returns the upload log only when debug is on', async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'kin-seed-log-'))
try {
Expand Down
18 changes: 17 additions & 1 deletion web/src/features/vm/detail-page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useMemo, useState } from 'react'
import {
useMutation,
useMutationState,
Expand Down Expand Up @@ -166,6 +166,19 @@ function VmDetailBody({
const proxy = ((data.proxy as VmProxySnap | undefined) ||
vm.proxy ||
{}) as VmProxySnap
const seedSlot = useMemo(() => {
const locale =
typeof vm.locale === 'string'
? vm.locale
: vm.locale === null
? null
: undefined
return {
timezone: vm.timezone,
timezone_source: vm.timezone_source,
locale,
}
}, [vm.timezone, vm.timezone_source, vm.locale])
const test = useVmTestChat(vm)
const refreshAll = async () => {
await Promise.all([
Expand Down Expand Up @@ -241,6 +254,7 @@ function VmDetailBody({
}),
onSuccess: async () => {
toast.success('已播种')
await refreshAll()
await qc.invalidateQueries({
queryKey: ['panel', 'vm', id, 'seed'],
})
Expand Down Expand Up @@ -583,12 +597,14 @@ function VmDetailBody({
{seed.data ? (
<SeedPolicyCard
seed={seed.data}
slot={seedSlot}
telemetry={kernel?.telemetry}
saving={saveSeed.isPending}
syncTelemetry={syncTelemetry}
debug={seedDebug}
onDebug={setSeedDebug}
uploadLog={seedUploadLog.data}
proxyTimezone={proxy.geo?.timezone || ''}
onSave={(next) => saveSeed.mutate(next)}
/>
) : (
Expand Down
3 changes: 3 additions & 0 deletions web/src/features/vm/seed-policy-card.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ describe('seed save follows the telemetry bit', () => {
claude_setting: 'A=1',
env: {},
theme: 'dark',
timezoneFollow: true,
timezoneFixed: '',
locale: 'en_US.UTF-8',
})
expect(telemetryEnabled(body)).toBe(true)
expect(body.disable_nonessential_traffic).toBe(true)
Expand Down
Loading
Loading