Skip to content

Commit f8f507f

Browse files
kilo-code-bot[bot]github-actions[bot]St0rmz1
authored
feat(kiloclaw): bump openclaw to version 2026.6.5 (#3932)
* feat(kiloclaw): bump openclaw to version 2026.6.5 * fix(kiloclaw): adapt controller to OpenClaw 2026.6.x behavior changes OpenClaw 2026.6.x changed two behaviors the controller relied on: - Auth profiles moved to SQLite: doctor imports legacy auth-profiles.json into a per-agent SQLite store and leaves a world-readable (0644) plaintext *.sqlite-import.*.bak. Remove that backup in the onboard/doctor path so no plaintext provider key remains on disk (chmod 0600 fallback). - agents delete now recursively removes the workspace. Verify the CLI-reported workspace path and report an accurate filesystemDisposition (deleted/retained) instead of a hardcoded 'unverified'; the worker schema accepts all three for rollout safety. Update controller/entrypoint smoke tests to the new SQLite auth store, backup removal, and workspace deletion, with unit coverage for both. * fix(kiloclaw): convert auth-profiles keyRef before doctor's SQLite import OpenClaw 2026.6.1+ `doctor --fix` imports auth-profiles.json into per-agent SQLite, storing a plaintext `key` verbatim when no `keyRef` is present. The controller's keyRef conversion ran after doctor, so on an upgrade whose JSON still held a plaintext kilocode key, doctor baked the plaintext into the SQLite auth store (the JSON rewrite afterward only touched the now-ignored backup). Run the idempotent keyRef migration once more BEFORE doctor in the existing- config path so the SQLite import lands a keyRef, never plaintext. The post-doctor call stays for self-healing on versions that still read the JSON. Add a bootstrap ordering assertion (migration before doctor) and a smoke assertion that no plaintext key survives in the SQLite auth store after an upgrade boot. * fix(kiloclaw): build-local-image --openclaw-tag silently built the wrong version The Dockerfile pin is `npm install -g pnpm openclaw@<ver>`, but the patch sed anchored on `-g openclaw@` (no `pnpm`), so it matched nothing and built the unchanged pin while printing 'Patching Dockerfile…'. Anchor on `openclaw@<ver>` directly and fail loudly if the pin isn't rewritten. * fix(kiloclaw): address SQLite auth-migration review findings * fix(kiloclaw): migrate auth in env-selected agent dirs (OPENCLAW_AGENT_DIR) * fix(kiloclaw): harden all auth backup suffixes, on doctor failure, with ~-expanded env dirs * refactor(kiloclaw): scope auth migration to the standard agent layout * fix(kiloclaw): fail closed if a pre-doctor keyRef rewrite fails * docs(claw): announce OpenClaw 2026.6.5 Early Access + SQLite credential migration * fix(kiloclaw) morning briefing plugin registration with OpenClaw --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: St0rmz1 <astorms@replicated.com>
1 parent e75ea53 commit f8f507f

17 files changed

Lines changed: 716 additions & 938 deletions

apps/web/src/app/(app)/claw/components/changelog-data.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ export type ChangelogEntry = {
1010

1111
// Newest entries first. Developers add new entries to the top of this array.
1212
export const CHANGELOG_ENTRIES: ChangelogEntry[] = [
13+
{
14+
date: '2026-06-10',
15+
description:
16+
'OpenClaw 2026.6.5 is available now as an Early Access upgrade. This release includes a new OpenClaw feature that migrates some credentials stored in JSON files into a SQLite database. The migration runs automatically the first time you upgrade — and once your instance has migrated, there is no automatic way to revert to the previous format.',
17+
category: 'feature',
18+
deployHint: 'upgrade_required',
19+
},
1320
{
1421
date: '2026-06-03',
1522
description:

pnpm-lock.yaml

Lines changed: 134 additions & 830 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

services/kiloclaw/Dockerfile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ RUN git config --system url."https://github.com/".insteadOf "ssh://git@github.co
6464
# under boot load routinely need 27-35s for the first TLS+fetch to complete.
6565
# Match the KiloCode chunk explicitly because OpenClaw also ships other provider
6666
# discovery timeout constants. Remove this patch once the timeout is configurable.
67-
RUN npm install -g pnpm openclaw@2026.5.26 clawhub mcporter@0.7.3 @steipete/summarize@0.12.0 @kilocode/cli@7.2.31 \
67+
RUN npm install -g pnpm openclaw@2026.6.5 clawhub mcporter@0.7.3 @steipete/summarize@0.12.0 @kilocode/cli@7.2.31 \
6868
&& OC_DIST=/usr/local/lib/node_modules/openclaw/dist \
6969
&& PM_FILES=$(grep -l 'KILOCODE_MODELS_URL' "$OC_DIST"/provider-models-*.js 2>/dev/null || true) \
7070
&& PM_FILE_COUNT=$(printf '%s\n' "$PM_FILES" | grep -c .) \
@@ -98,7 +98,7 @@ RUN COMPOSIO_INSTALL_TAG="%40composio%2Fcli%40${COMPOSIO_CLI_VERSION}" \
9898
# Bake bundled plugin runtime deps into OpenClaw's external stage dir so first
9999
# boot is pure startup — no npm install from `openclaw doctor` or gateway plugin
100100
# load on shared-cpu Fly machines. The stage-root naming mirrors OpenClaw
101-
# 2026.5.26's resolveExternalBundledRuntimeDepsInstallRoot().
101+
# 2026.6.5's resolveExternalBundledRuntimeDepsInstallRoot().
102102
RUN cd /usr/local/lib/node_modules/openclaw \
103103
&& OPENCLAW_PACKAGE_ROOT="$(pwd -P)" \
104104
&& OPENCLAW_PACKAGE_VERSION="$(node -p "require('./package.json').version")" \
@@ -257,8 +257,8 @@ RUN mkdir -p /root/.openclaw \
257257
&& mkdir -p /root/clawd/skills
258258

259259
# Copy helper scripts (used at runtime by the controller/gateway)
260-
# Build cache bust: 2026-06-01-v69-openclaw-2026.5.26
261-
RUN echo "14"
260+
# Build cache bust: 2026-06-10-v70-openclaw-2026.6.5
261+
RUN echo "15"
262262
COPY openclaw-pairing-list.js /usr/local/bin/openclaw-pairing-list.js
263263
COPY openclaw-device-pairing-list.js /usr/local/bin/openclaw-device-pairing-list.js
264264

services/kiloclaw/controller/src/auth-profiles-migration.test.ts

Lines changed: 186 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, it, vi } from 'vitest';
22
import {
33
migrateKilocodeAuthProfilesToKeyRef,
4+
hardenAuthProfileMigrationBackups,
45
type AuthProfilesMigrationDeps,
56
} from './auth-profiles-migration';
67

@@ -113,7 +114,12 @@ describe('migrateKilocodeAuthProfilesToKeyRef', () => {
113114

114115
const report = migrateKilocodeAuthProfilesToKeyRef(ROOT, fsDeps(fs));
115116

116-
expect(report).toEqual({ filesScanned: 1, filesModified: 1, profilesMigrated: 1 });
117+
expect(report).toEqual({
118+
filesScanned: 1,
119+
filesModified: 1,
120+
profilesMigrated: 1,
121+
filesFailed: 0,
122+
});
117123

118124
const written = parseStore(fs.files.get(MAIN));
119125
expect(written.profiles['kilocode:default']).toEqual({
@@ -133,7 +139,12 @@ describe('migrateKilocodeAuthProfilesToKeyRef', () => {
133139
const before = fs.files.get(MAIN);
134140
const report = migrateKilocodeAuthProfilesToKeyRef(ROOT, fsDeps(fs));
135141

136-
expect(report).toEqual({ filesScanned: 1, filesModified: 0, profilesMigrated: 0 });
142+
expect(report).toEqual({
143+
filesScanned: 1,
144+
filesModified: 0,
145+
profilesMigrated: 0,
146+
filesFailed: 0,
147+
});
137148
expect(fs.files.get(MAIN)).toBe(before);
138149
});
139150

@@ -205,7 +216,12 @@ describe('migrateKilocodeAuthProfilesToKeyRef', () => {
205216

206217
const report = migrateKilocodeAuthProfilesToKeyRef(ROOT, fsDeps(fs));
207218

208-
expect(report).toEqual({ filesScanned: 2, filesModified: 2, profilesMigrated: 2 });
219+
expect(report).toEqual({
220+
filesScanned: 2,
221+
filesModified: 2,
222+
profilesMigrated: 2,
223+
filesFailed: 0,
224+
});
209225
});
210226

211227
it('skips malformed JSON with a warning but does not throw', () => {
@@ -217,7 +233,12 @@ describe('migrateKilocodeAuthProfilesToKeyRef', () => {
217233

218234
const report = migrateKilocodeAuthProfilesToKeyRef(ROOT, fsDeps(fs));
219235

220-
expect(report).toEqual({ filesScanned: 1, filesModified: 0, profilesMigrated: 0 });
236+
expect(report).toEqual({
237+
filesScanned: 1,
238+
filesModified: 0,
239+
profilesMigrated: 0,
240+
filesFailed: 0,
241+
});
221242
expect(warnSpy).toHaveBeenCalled();
222243
warnSpy.mockRestore();
223244
});
@@ -243,7 +264,38 @@ describe('migrateKilocodeAuthProfilesToKeyRef', () => {
243264

244265
const report = migrateKilocodeAuthProfilesToKeyRef(ROOT, fsDeps(fs));
245266

246-
expect(report).toEqual({ filesScanned: 0, filesModified: 0, profilesMigrated: 0 });
267+
expect(report).toEqual({
268+
filesScanned: 0,
269+
filesModified: 0,
270+
profilesMigrated: 0,
271+
filesFailed: 0,
272+
});
273+
});
274+
275+
it('reports filesFailed (and migrates nothing) when the rewrite throws', () => {
276+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
277+
const fs = createFs();
278+
seedDir(fs, `${ROOT}/agents`);
279+
seedDir(fs, `${ROOT}/agents/main`);
280+
seedFile(fs, MAIN, JSON.stringify(plaintextStore()));
281+
const deps: AuthProfilesMigrationDeps = {
282+
...fsDeps(fs),
283+
writeFileSync: () => {
284+
throw new Error('ENOSPC');
285+
},
286+
};
287+
288+
const report = migrateKilocodeAuthProfilesToKeyRef(ROOT, deps);
289+
290+
expect(report).toEqual({
291+
filesScanned: 1,
292+
filesModified: 0,
293+
profilesMigrated: 0,
294+
filesFailed: 1,
295+
});
296+
// The plaintext is still on disk because the write failed.
297+
expect(fs.files.get(MAIN)).toContain('secret-literal-key');
298+
warnSpy.mockRestore();
247299
});
248300

249301
it('preserves unrelated profile fields when migrating', () => {
@@ -279,4 +331,133 @@ describe('migrateKilocodeAuthProfilesToKeyRef', () => {
279331
metadata: { createdBy: 'onboard' },
280332
});
281333
});
334+
335+
it('converts the historical api_key alias (not just key) to a keyRef', () => {
336+
const fs = createFs();
337+
seedDir(fs, `${ROOT}/agents`);
338+
seedDir(fs, `${ROOT}/agents/main`);
339+
seedFile(
340+
fs,
341+
MAIN,
342+
JSON.stringify({
343+
version: 1,
344+
profiles: {
345+
'kilocode:default': {
346+
type: 'api_key',
347+
provider: 'kilocode',
348+
api_key: 'legacy-alias-key',
349+
},
350+
},
351+
})
352+
);
353+
354+
const report = migrateKilocodeAuthProfilesToKeyRef(ROOT, fsDeps(fs));
355+
356+
expect(report.profilesMigrated).toBe(1);
357+
const profile = parseStore(fs.files.get(MAIN)).profiles['kilocode:default'];
358+
expect(profile).toEqual({
359+
type: 'api_key',
360+
provider: 'kilocode',
361+
keyRef: { source: 'env', provider: 'default', id: 'KILOCODE_API_KEY' },
362+
});
363+
expect(profile).not.toHaveProperty('api_key');
364+
expect(profile).not.toHaveProperty('key');
365+
});
366+
});
367+
368+
describe('hardenAuthProfileMigrationBackups', () => {
369+
const ROOT = '/root/.openclaw';
370+
const AGENT_DIR = `${ROOT}/agents/main/agent`;
371+
const BAK = `${AGENT_DIR}/auth-profiles.json.sqlite-import.1781117548412.bak`;
372+
373+
function seedAgentDir(fs: InMemoryFs): void {
374+
seedDir(fs, `${ROOT}/agents`);
375+
seedDir(fs, `${ROOT}/agents/main`);
376+
seedDir(fs, AGENT_DIR);
377+
}
378+
379+
it('tightens the sqlite-import backup to 0o600 and retains it (recovery copy)', () => {
380+
const fs = createFs();
381+
seedAgentDir(fs);
382+
seedFile(fs, BAK, JSON.stringify(plaintextStore()));
383+
const chmodSync = vi.fn();
384+
385+
const report = hardenAuthProfileMigrationBackups(ROOT, { ...fsDeps(fs), chmodSync });
386+
387+
expect(report).toEqual({ dirsScanned: 1, backupsHardened: 1, backupsFailed: 0 });
388+
expect(chmodSync).toHaveBeenCalledWith(BAK, 0o600);
389+
// The backup is retained, not deleted.
390+
expect(fs.files.has(BAK)).toBe(true);
391+
});
392+
393+
it('hardens every known migration backup suffix and leaves unrelated files alone', () => {
394+
const fs = createFs();
395+
seedAgentDir(fs);
396+
const backups = [
397+
`${AGENT_DIR}/auth-profiles.json.sqlite-import.1.bak`,
398+
`${AGENT_DIR}/auth-profiles.json.legacy-flat.2.bak`,
399+
`${AGENT_DIR}/auth-profiles.json.api-key-alias.3.bak`,
400+
`${AGENT_DIR}/auth-profiles.json.aws-sdk-profile.4.bak`,
401+
`${AGENT_DIR}/auth-profiles.json.openai-provider-unification.5.bak`,
402+
`${AGENT_DIR}/auth-profiles.json.oauth-ref.6.bak`,
403+
];
404+
for (const b of backups) seedFile(fs, b, JSON.stringify(plaintextStore()));
405+
// Not migration backups — must be left untouched.
406+
for (const keep of [
407+
`${AGENT_DIR}/openclaw-agent.sqlite`,
408+
`${AGENT_DIR}/models.json`,
409+
`${AGENT_DIR}/unrelated.bak`,
410+
`${AGENT_DIR}/auth-profiles.json.unknown-migration.7.bak`,
411+
]) {
412+
seedFile(fs, keep, 'x');
413+
}
414+
const chmodSync = vi.fn();
415+
416+
const report = hardenAuthProfileMigrationBackups(ROOT, { ...fsDeps(fs), chmodSync });
417+
418+
expect(report.backupsHardened).toBe(backups.length);
419+
expect(chmodSync).toHaveBeenCalledTimes(backups.length);
420+
for (const b of backups) expect(chmodSync).toHaveBeenCalledWith(b, 0o600);
421+
});
422+
423+
it('counts a failure when chmod throws, without throwing', () => {
424+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
425+
const fs = createFs();
426+
seedAgentDir(fs);
427+
seedFile(fs, BAK, JSON.stringify(plaintextStore()));
428+
const deps: AuthProfilesMigrationDeps = {
429+
...fsDeps(fs),
430+
chmodSync: () => {
431+
throw new Error('EPERM');
432+
},
433+
};
434+
435+
const report = hardenAuthProfileMigrationBackups(ROOT, deps);
436+
437+
expect(report).toEqual({ dirsScanned: 1, backupsHardened: 0, backupsFailed: 1 });
438+
warnSpy.mockRestore();
439+
});
440+
441+
it('scans multiple agent directories', () => {
442+
const fs = createFs();
443+
seedDir(fs, `${ROOT}/agents`);
444+
for (const agent of ['main', 'coding']) {
445+
seedDir(fs, `${ROOT}/agents/${agent}`);
446+
seedDir(fs, `${ROOT}/agents/${agent}/agent`);
447+
seedFile(fs, `${ROOT}/agents/${agent}/agent/auth-profiles.json.sqlite-import.99.bak`, 'x');
448+
}
449+
const chmodSync = vi.fn();
450+
451+
const report = hardenAuthProfileMigrationBackups(ROOT, { ...fsDeps(fs), chmodSync });
452+
453+
expect(report).toEqual({ dirsScanned: 2, backupsHardened: 2, backupsFailed: 0 });
454+
});
455+
456+
it('is a no-op when no agent dirs exist', () => {
457+
const fs = createFs();
458+
459+
const report = hardenAuthProfileMigrationBackups(ROOT, fsDeps(fs));
460+
461+
expect(report).toEqual({ dirsScanned: 0, backupsHardened: 0, backupsFailed: 0 });
462+
});
282463
});

0 commit comments

Comments
 (0)