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 @@

## 未发布

- 槽位种子页:`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)
- 面板:侧栏「GitHub / 文档」两枚链接不再折行,字体偏宽时「文档」不会逐字竖排,空间不够先缩箭头再省略文字;VM 列表搜索框关闭浏览器自动填充,不再被回填成登录邮箱。
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` | 播种;强制保留 telemetry/bedrock/vertex 等 env;`claude_setting` 可编辑并写入槽内 `.claude/claude.setting` |
| 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 行脱敏上传审计,默认不带) |
| 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
10 changes: 10 additions & 0 deletions docs/TELEMETRY.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,13 @@ An update must verify the new binary hash and the replacement process before
reporting success. A failed service restart is reported per slot so the old VM
identity remains intact and can be rolled back by republishing the previous
binary.

## Upload audit log (debug)

Each successful or failed `PostBatch` / GrowthBook eval append one line to
`vms/<id>/run/telemetry-upload.log` (next to `worker.json`). The file keeps at
most 40 lines. Payload JSON is the request shape (`events` wrapper for batch
uploads) with identity fields stripped (email, account/org UUIDs, device and
session ids). The panel seed tab exposes this only when **调试上传** is on,
via `GET /api/panel/vms/:id/seed-settings?debug=1` → `telemetry_log` (string
array, last 40 lines).
35 changes: 22 additions & 13 deletions src/lib/admin/panel-routes.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3542,19 +3542,18 @@ export function createPanelHandler(ctx) {
} catch {}
const seed_policy = defaultSeedPolicy(vm.seed_policy || {})
if (claude_setting !== null) seed_policy.claude_setting = claude_setting
return json(
res,
200,
panel.ok({
vm_id: id,
seed_policy,
...seedTelemetryContract(seed_policy),
settings_json,
claude_setting,
kin_seed,
cli_home: homeDir,
}),
)
const debug = new URL(req.url || '/', 'http://localhost').searchParams.get('debug') === '1'
const payload = {
vm_id: id,
seed_policy,
...seedTelemetryContract(seed_policy),
settings_json,
claude_setting,
kin_seed,
cli_home: homeDir,
}
if (debug) payload.telemetry_log = readTelemetryUploadLog(cfg.paths.project, id)
return json(res, 200, panel.ok(payload))
}
// PUT /api/panel/vms/:id/seed-settings
if (req.method === 'PUT' && /^\/api\/panel\/vms\/[^/]+\/seed-settings$/.test(p)) {
Expand Down Expand Up @@ -5778,3 +5777,13 @@ export function createPanelHandler(ctx) {
return false
}
}

function readTelemetryUploadLog(projectRoot, id) {
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,80}$/.test(String(id || ''))) return []
try {
const text = fs.readFileSync(path.join(projectRoot, 'vms', id, 'run', 'telemetry-upload.log'), 'utf8')
return text.split(/\r?\n/).filter(Boolean).slice(-40)
} catch {
return []
}
}
1 change: 1 addition & 0 deletions src/lib/protocol/seed-policy.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ export function buildSlotSettingsEnv(pol = {}, { timezone, locale, extra } = {})
const out = applyRequiredSeedEnv(env, pol)
// 2.1.283+ sends x-claude-code-prompt-id only when this is set. Default on.
out.CLAUDE_CODE_GATEWAY_HINT_HEADERS = '1'
if (out.CLAUDE_CODE_ATTRIBUTION_HEADER == null) out.CLAUDE_CODE_ATTRIBUTION_HEADER = '0'
return out
}

Expand Down
32 changes: 32 additions & 0 deletions test/unit/panel-vm-seed-settings.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,35 @@ test('GET seed-settings returns an empty claude.setting file as empty', async ()
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 {
seed(root, 'vm-01')
const run = path.join(root, 'vms', 'vm-01', 'run')
fs.mkdirSync(run, { recursive: true })
fs.writeFileSync(
path.join(run, 'telemetry-upload.log'),
'old\n2026-10-19T00:00:00Z batch tengu_init ok {"event_type":"x"}\n',
)
const hidden = handler(root)
await hidden.handlePanel(
{ method: 'GET', url: '/api/panel/vms/vm-01/seed-settings' },
{},
new URL('http://localhost/api/panel/vms/vm-01/seed-settings'),
)
assert.equal(hidden.response.body.data.telemetry_log, undefined)
const shown = handler(root)
await shown.handlePanel(
{ method: 'GET', url: '/api/panel/vms/vm-01/seed-settings?debug=1' },
{},
new URL('http://localhost/api/panel/vms/vm-01/seed-settings?debug=1'),
)
assert.deepEqual(shown.response.body.data.telemetry_log, [
'old',
'2026-10-19T00:00:00Z batch tengu_init ok {"event_type":"x"}',
])
} finally {
fs.rmSync(root, { recursive: true, force: true })
}
})
1 change: 1 addition & 0 deletions test/unit/seed-policy.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ test('buildSlotSettingsEnv is TZ/LANG plus contract, leftover extra cannot win',
assert.equal(env.LC_ALL, 'en_US.UTF-8')
assert.equal(env.CUSTOM, 'ok')
assert.equal(env.CLAUDE_CODE_GATEWAY_HINT_HEADERS, '1')
assert.equal(env.CLAUDE_CODE_ATTRIBUTION_HEADER, '0')
})

test('buildSeedSettingsEnv delegates to slot env', () => {
Expand Down
15 changes: 14 additions & 1 deletion web/src/features/vm/detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import { probeOutcome, type ProbeCheck } from '@/features/vm/probe-status'
import {
vmQueryOptions,
vmSeedQueryOptions,
vmSeedUploadLogQueryOptions,
vmsListQueryOptions,
} from '@/features/vm/queries'
import { RuntimeChip } from '@/features/vm/runtime-chip'
Expand Down Expand Up @@ -117,6 +118,7 @@ function VmDetailBody({
onTabChange: (tab: string) => void
}) {
const navigate = useNavigate()
const [seedDebug, setSeedDebug] = useState(false)
const qc = useQueryClient()
const perms = useVmPerms()
const dash = useQuery({
Expand All @@ -138,6 +140,11 @@ function VmDetailBody({
enabled: perms.admin && wantedTab === 'seed',
refetchInterval: wantedTab === 'seed' ? 2000 : false,
})
const seedUploadLog = useQuery({
...vmSeedUploadLogQueryOptions(id),
enabled: perms.admin && wantedTab === 'seed' && seedDebug,
refetchInterval: seedDebug && wantedTab === 'seed' ? 2000 : false,
})
const routing = useQuery({ ...routingQueryOptions(), enabled: perms.admin })
usePoolSeatStream()

Expand Down Expand Up @@ -235,7 +242,10 @@ function VmDetailBody({
onSuccess: async () => {
toast.success('已播种')
await qc.invalidateQueries({
queryKey: vmSeedQueryOptions(id).queryKey,
queryKey: ['panel', 'vm', id, 'seed'],
})
await qc.invalidateQueries({
queryKey: ['panel', 'vm', id, 'seed-upload-log'],
})
},
onError: (error: Error) => toast.error(error.message),
Expand Down Expand Up @@ -576,6 +586,9 @@ function VmDetailBody({
telemetry={kernel?.telemetry}
saving={saveSeed.isPending}
syncTelemetry={syncTelemetry}
debug={seedDebug}
onDebug={setSeedDebug}
uploadLog={seedUploadLog.data}
onSave={(next) => saveSeed.mutate(next)}
/>
) : (
Expand Down
19 changes: 19 additions & 0 deletions web/src/features/vm/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,25 @@ export function vmSeedQueryOptions(id: string) {
})
}

/** Upload audit lines only — does not share the seed form query key. */
export function vmSeedUploadLogQueryOptions(id: string) {
return queryOptions({
queryKey: ['panel', 'vm', id, 'seed-upload-log'] as const,
queryFn: async () => {
const data = await api<VmSeedSettings>(
`/api/panel/vms/${encodeURIComponent(id)}/seed-settings?debug=1`
)
const lines = data.telemetry_log
return Array.isArray(lines)
? lines.filter(
(line): line is string => typeof line === 'string' && line !== ''
)
: []
},
refetchInterval: false,
})
}

export function vmCredentialQueryOptions(id: string, enabled = true) {
return queryOptions({
queryKey: ['panel', 'vm', id, 'credential'] as const,
Expand Down
2 changes: 2 additions & 0 deletions web/src/features/vm/seed-policy-card.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ describe('seed save follows the telemetry bit', () => {
reject_client_settings: true,
reject_client_metadata_identity: true,
claude_setting: 'A=1',
env: {},
theme: 'dark',
})
expect(telemetryEnabled(body)).toBe(true)
expect(body.disable_nonessential_traffic).toBe(true)
Expand Down
Loading
Loading