Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
94 commits
Select commit Hold shift + click to select a range
a0dbd93
feat(kiloclaw): add Google account credential storage and env plumbing
iscekic Mar 9, 2026
349f2ef
feat(kiloclaw): add Google credentials API endpoint and controller gw…
iscekic Mar 9, 2026
bfa37c7
feat(kiloclaw): add google-setup Docker image scaffold
iscekic Mar 9, 2026
95f953f
feat(kiloclaw): add public-key endpoint for google-setup encryption
iscekic Mar 9, 2026
2e267e9
fix(kiloclaw): fix google-setup auth flow and add user-facing routes
iscekic Mar 9, 2026
707dc67
chore(kiloclaw): add .dockerignore and note gws CLI dependency
iscekic Mar 9, 2026
4369257
feat(kiloclaw): add Next.js Google credentials integration
iscekic Mar 9, 2026
e61fed7
fix(kiloclaw): address review feedback on Google credentials feature
iscekic Mar 9, 2026
4cd62bd
fix(kiloclaw): use correct gws CLI package in Dockerfile
iscekic Mar 9, 2026
f6fc259
merge: resolve conflicts with main (secret catalog + google credentials)
iscekic Mar 10, 2026
db03f1d
test(kiloclaw): add Google credentials integration test
iscekic Mar 10, 2026
ec12793
feat(kiloclaw): add gcloud to Docker image, own OAuth flow, and e2e test
iscekic Mar 10, 2026
06a1b00
feat(claw): add Google Account section to Settings tab
iscekic Mar 10, 2026
a5949a4
feat(claw): auto-fill API key in Google setup command
iscekic Mar 10, 2026
6f75aa9
feat(kiloclaw): use GHCR for google-setup image and add publishing docs
iscekic Mar 10, 2026
1291d2a
fix(claw): void floating clipboard promise in GoogleAccountSection
iscekic Mar 10, 2026
eac53f6
feat(claw): add --worker-url flag to Google setup command in dev mode
iscekic Mar 10, 2026
e4f53aa
feat(kiloclaw): replace gog with gws CLI in container image
iscekic Mar 10, 2026
4dd8c23
feat(kiloclaw): install gws agent skills on startup when Google is co…
iscekic Mar 10, 2026
fdeffbc
fix(claw): clarify activation message in google-setup script
iscekic Mar 10, 2026
bee01dd
fix(kiloclaw): set GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE for gws auth
iscekic Mar 10, 2026
223990c
fix(kiloclaw): don't block on provision in integration test
iscekic Mar 10, 2026
49ad632
fix(claw): add type field to Google credentials JSON
iscekic Mar 10, 2026
dc0ec6c
fix(claw): auto-answer gws auth login prompt using expect
iscekic Mar 10, 2026
550f00c
fix(claw): inline disconnect button with Google connected badge
iscekic Mar 10, 2026
17fe88b
fix(kiloclaw): harden Google credentials feature from code review
iscekic Mar 10, 2026
7ee4c5e
fix(kiloclaw): address second-pass review findings
iscekic Mar 11, 2026
d97e2d6
fix(kiloclaw): address third-pass review findings
iscekic Mar 11, 2026
f1e5a4f
fix(kiloclaw): remove stale gws token cache on credential write
iscekic Mar 11, 2026
1c4a1de
fix(kiloclaw): only ignore ENOENT in gws credential cleanup
iscekic Mar 11, 2026
b01436d
fix(kiloclaw): pin skills CLI version in gws agent skills install
iscekic Mar 11, 2026
f20513b
fix(kiloclaw): fail integration test when DO never becomes reachable
iscekic Mar 11, 2026
95e4954
fix(kiloclaw): check provision response in google-setup e2e test
iscekic Mar 11, 2026
520b96d
merge: integrate latest main into feat/kiloclaw-google-account-access
iscekic Mar 11, 2026
93781fe
fix(claw): remove unused AlertCircle import from SettingsTab
iscekic Mar 11, 2026
4ef1e23
fix(kiloclaw): bind OAuth callback to loopback and refresh setup JWT
iscekic Mar 11, 2026
23593af
merge: integrate latest main into feat/kiloclaw-google-account-access
iscekic Mar 11, 2026
47a4cfa
Merge remote-tracking branch 'origin/main' into feat/kiloclaw-google-…
iscekic Mar 11, 2026
261d102
refactor(kiloclaw): move Google credential env vars to secret-catalog
iscekic Mar 11, 2026
a5f27cc
fix(kiloclaw): wrap Google credential decryption in try/catch
iscekic Mar 11, 2026
343c7c2
fix(kiloclaw): deduplicate OAuth client_secret across credential enve…
iscekic Mar 11, 2026
99ea1d7
fix(kiloclaw): remove unused connectGoogle mutation
iscekic Mar 11, 2026
74b0cce
fix(kiloclaw): move public-key endpoint behind auth
iscekic Mar 11, 2026
3c92b09
feat(kiloclaw): add changelog entry for gws CLI availability
iscekic Mar 11, 2026
b5e4590
docs(kiloclaw): document env mutation side effect in writeGwsCredentials
iscekic Mar 11, 2026
1532c97
chore(kiloclaw): add engines field to google-setup package.json
iscekic Mar 11, 2026
5fb3d95
fix(kiloclaw): pass DATABASE_URL via env in integration test sql helper
iscekic Mar 11, 2026
c749aa1
fix(kiloclaw): restore gog binary alongside gws in container image
iscekic Mar 11, 2026
ecdbb0b
fix(kiloclaw): validate --worker-url scheme in google-setup
iscekic Mar 11, 2026
0c33a65
fix(kiloclaw): fix corrupted envelope test to match EncryptedEnvelope…
iscekic Mar 11, 2026
d45fa5e
docs(kiloclaw): add gws-to-gog migration plan
iscekic Mar 11, 2026
7295137
chore(kiloclaw): add jose dependency to controller for JWE keyring
iscekic Mar 11, 2026
e816c6a
feat(kiloclaw): add gog-credentials module with JWE keyring support
iscekic Mar 11, 2026
4c1f0b9
refactor(kiloclaw): replace gws-credentials with gog-credentials in c…
iscekic Mar 11, 2026
8ab25f3
chore(kiloclaw): remove gws CLI from container image
iscekic Mar 11, 2026
d52cee5
chore(kiloclaw): remove gws and expect from google-setup image
iscekic Mar 11, 2026
7d89bc4
feat(kiloclaw): rewrite google-setup to use gcloud + gog instead of gws
iscekic Mar 11, 2026
aa48c6d
docs(kiloclaw): update google-setup README for gog migration
iscekic Mar 11, 2026
1244e47
refactor(kiloclaw): rename test/ to e2e/
iscekic Mar 11, 2026
3094457
chore: remove gws entry from changelog
iscekic Mar 11, 2026
3768361
refactor: replace api key with token
iscekic Mar 11, 2026
4e3017b
fix(kiloclaw): remove Keep API scope (not available for desktop OAuth)
iscekic Mar 11, 2026
7293589
feat(kiloclaw): numbered project picker for existing projects in goog…
iscekic Mar 11, 2026
a6aebd2
fix(kiloclaw): use snake_case JSON fields in gog keyring entries
iscekic Mar 11, 2026
7ec9787
fix(kiloclaw): wrap gog keyring data in 99designs/keyring Item struct
iscekic Mar 11, 2026
585482f
fix(kiloclaw): write gog config.json with keyring_backend=file
iscekic Mar 11, 2026
3232736
fix(kiloclaw): export GOG_KEYRING_PASSWORD in start-openclaw.sh
iscekic Mar 11, 2026
9858240
fix(google-setup): bind OAuth callback server to all interfaces
iscekic Mar 11, 2026
1fa8a4c
docs: remove outdated gws-to-gog migration plan doc
iscekic Mar 11, 2026
2c879bb
feat(kiloclaw): ship gog config as tarball instead of reconstructing …
iscekic Mar 11, 2026
f7607e1
fix: use execFileSync for tar extraction, fix stale GoogleCredentials…
iscekic Mar 11, 2026
2dead7b
fix(e2e): update google-credentials integration test to match current…
iscekic Mar 11, 2026
dde26fc
fix(e2e): update google-setup e2e test to read .dev.vars and handle s…
iscekic Mar 11, 2026
e26c3d3
Merge branch 'main' into feat/kiloclaw-google-account-access
iscekic Mar 11, 2026
7dced68
fix(kiloclaw): bump encryptedData max from 32 KiB to 64 KiB
iscekic Mar 11, 2026
f3ddeeb
fix(kiloclaw): clarify public key cache is isolate-level, not persistent
iscekic Mar 11, 2026
fff5bb5
fix(google-setup): fix stale step numbering in setup.mjs
iscekic Mar 11, 2026
3fe8171
fix(kiloclaw): add cross-reference comments for GOG_KEYRING_PASSWORD
iscekic Mar 11, 2026
e4a0045
fix(google-setup): clarify utf8 encoding param in encryptEnvelope
iscekic Mar 11, 2026
9c862f0
fix(claw): disable refetchOnWindowFocus for Google setup command query
iscekic Mar 11, 2026
5e25825
fix(kiloclaw): remove stale config dir before extracting new gog tarball
iscekic Mar 11, 2026
48e67b6
fix(kiloclaw): harden tar extraction with --no-absolute-names
iscekic Mar 11, 2026
38c93e9
fix(kiloclaw): clear gog env vars on disconnect cleanup
iscekic Mar 11, 2026
04409a9
fix(google-setup): fail on any non-OK response in auth preflight
iscekic Mar 11, 2026
94a96d8
refactor(controller): await writeGogCredentials before gateway startup
iscekic Mar 11, 2026
0a9295f
feat(kiloclaw/google-setup): add arch-aware gogcli install in Dockerfile
iscekic Mar 11, 2026
a004979
fix(google-setup): verify credentials before tarballing
iscekic Mar 11, 2026
3c31fca
merge main into feat/kiloclaw-google-account-access
iscekic Mar 11, 2026
9c04098
fix(controller): remove --no-absolute-names from tar extraction
iscekic Mar 12, 2026
b2b22b8
chore: revert random llm change
iscekic Mar 12, 2026
643a470
chore: revert bad merge stuff
iscekic Mar 12, 2026
a57b4d4
Merge remote-tracking branch 'origin/main' into feat/kiloclaw-google-…
iscekic Mar 12, 2026
bf8ce1d
fix(ui): add confirmation dialog before Google account disconnect
iscekic Mar 12, 2026
9bfac3b
fix(config): unify GOG_KEYRING_PASSWORD usage across setup and runtime
iscekic Mar 12, 2026
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: 0 additions & 1 deletion kiloclaw/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ RUN npm install -g mcporter@0.7.3
# Install summarize (web page summarization CLI)
RUN npm install -g @steipete/summarize@0.11.1


# Install Go (available at runtime for users to `go install` additional tools)
ENV GO_VERSION=1.26.0
RUN ARCH="$(dpkg --print-architecture)" \
Expand Down
128 changes: 128 additions & 0 deletions kiloclaw/controller/src/gog-credentials.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';

function mockDeps() {
return {
mkdirSync: vi.fn(),
writeFileSync: vi.fn(),
unlinkSync: vi.fn(),
rmSync: vi.fn(),
execFileSync: vi.fn(),
};
}

// A tiny valid .tar.gz base64 — content doesn't matter for unit tests since execSync is mocked
const FAKE_TARBALL_BASE64 = Buffer.from('fake-tarball-data').toString('base64');

describe('writeGogCredentials', () => {
let writeGogCredentials: typeof import('./gog-credentials').writeGogCredentials;

beforeEach(async () => {
vi.resetModules();
const mod = await import('./gog-credentials');
writeGogCredentials = mod.writeGogCredentials;
});

it('extracts tarball and sets env vars when GOOGLE_GOG_CONFIG_TARBALL is set', async () => {
const deps = mockDeps();
const dir = '/root/.config/gogcli';
const env: Record<string, string | undefined> = {
GOOGLE_GOG_CONFIG_TARBALL: FAKE_TARBALL_BASE64,
GOOGLE_ACCOUNT_EMAIL: 'user@gmail.com',
};
const result = await writeGogCredentials(env, dir, deps);

expect(result).toBe(true);
// Should remove stale config before extracting
expect(deps.rmSync).toHaveBeenCalledWith(dir, { recursive: true, force: true });
expect(deps.mkdirSync).toHaveBeenCalledWith('/root/.config', { recursive: true });

// Should write temp tarball file
expect(deps.writeFileSync).toHaveBeenCalledWith(
'/root/.config/gogcli-config.tar.gz',
Buffer.from(FAKE_TARBALL_BASE64, 'base64')
);

expect(deps.execFileSync).toHaveBeenCalledWith('tar', [
'xzf',
'/root/.config/gogcli-config.tar.gz',
'-C',
'/root/.config',
]);

// Should clean up temp tarball
expect(deps.unlinkSync).toHaveBeenCalledWith('/root/.config/gogcli-config.tar.gz');

// Should set gog env vars
expect(env.GOG_KEYRING_BACKEND).toBe('file');
expect(env.GOG_KEYRING_PASSWORD).toBe('kiloclaw');
expect(env.GOG_ACCOUNT).toBe('user@gmail.com');
});

it('works without GOOGLE_ACCOUNT_EMAIL', async () => {
const deps = mockDeps();
const env: Record<string, string | undefined> = {
GOOGLE_GOG_CONFIG_TARBALL: FAKE_TARBALL_BASE64,
};
const result = await writeGogCredentials(env, '/root/.config/gogcli', deps);

expect(result).toBe(true);
expect(env.GOG_KEYRING_BACKEND).toBe('file');
expect(env.GOG_KEYRING_PASSWORD).toBe('kiloclaw');
expect(env.GOG_ACCOUNT).toBeUndefined();
});

it('returns false and cleans up when tarball env var is absent', async () => {
const deps = mockDeps();
const dir = '/root/.config/gogcli';
const result = await writeGogCredentials({}, dir, deps);

expect(result).toBe(false);
expect(deps.rmSync).toHaveBeenCalledWith(dir, { recursive: true, force: true });
expect(deps.mkdirSync).not.toHaveBeenCalled();
});

it('clears gog env vars when tarball env var is absent', async () => {
const deps = mockDeps();
const env: Record<string, string | undefined> = {
GOG_KEYRING_BACKEND: 'file',
GOG_KEYRING_PASSWORD: 'kiloclaw',
GOG_ACCOUNT: 'user@gmail.com',
};
await writeGogCredentials(env, '/root/.config/gogcli', deps);

expect(env.GOG_KEYRING_BACKEND).toBeUndefined();
expect(env.GOG_KEYRING_PASSWORD).toBeUndefined();
expect(env.GOG_ACCOUNT).toBeUndefined();
});

it('removes existing config dir before extracting new tarball', async () => {
const deps = mockDeps();
const callOrder: string[] = [];
deps.rmSync.mockImplementation(() => callOrder.push('rmSync'));
deps.mkdirSync.mockImplementation(() => callOrder.push('mkdirSync'));
deps.execFileSync.mockImplementation(() => callOrder.push('execFileSync'));

const env: Record<string, string | undefined> = {
GOOGLE_GOG_CONFIG_TARBALL: FAKE_TARBALL_BASE64,
};
await writeGogCredentials(env, '/root/.config/gogcli', deps);

expect(callOrder).toEqual(['rmSync', 'mkdirSync', 'execFileSync']);
});

it('cleans up temp tarball even if extraction fails', async () => {
const deps = mockDeps();
deps.execFileSync.mockImplementation(() => {
throw new Error('tar failed');
});

const env: Record<string, string | undefined> = {
GOOGLE_GOG_CONFIG_TARBALL: FAKE_TARBALL_BASE64,
};

await expect(writeGogCredentials(env, '/root/.config/gogcli', deps)).rejects.toThrow(
'tar failed'
);
expect(deps.unlinkSync).toHaveBeenCalledWith('/root/.config/gogcli-config.tar.gz');
});
});
96 changes: 96 additions & 0 deletions kiloclaw/controller/src/gog-credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/**
* Sets up gogcli credentials by extracting a pre-built config tarball.
*
* When the container starts with GOOGLE_GOG_CONFIG_TARBALL env var, this module:
* 1. Base64-decodes the tarball to a temp file
* 2. Extracts it to /root/.config/ (produces /root/.config/gogcli/)
* 3. Sets GOG_KEYRING_BACKEND, GOG_KEYRING_PASSWORD, GOG_ACCOUNT env vars
*/
import path from 'node:path';

const GOG_CONFIG_DIR = '/root/.config/gogcli';

export type GogCredentialsDeps = {
mkdirSync: (dir: string, opts: { recursive: boolean }) => void;
writeFileSync: (path: string, data: Buffer) => void;
unlinkSync: (path: string) => void;
rmSync: (path: string, opts: { recursive: boolean; force: boolean }) => void;
execFileSync: (file: string, args: string[]) => void;
};

/**
* Extract gog config tarball if the corresponding env var is set.
* Returns true if credentials were extracted, false if skipped.
*
* Side effect: mutates the passed `env` record by setting
* GOG_KEYRING_BACKEND, GOG_KEYRING_PASSWORD, and GOG_ACCOUNT.
*/
export async function writeGogCredentials(
env: Record<string, string | undefined> = process.env as Record<string, string | undefined>,
configDir = GOG_CONFIG_DIR,
deps?: Partial<GogCredentialsDeps>
): Promise<boolean> {
const fs = await import('node:fs');
const cp = await import('node:child_process');
const d: GogCredentialsDeps = {
mkdirSync: deps?.mkdirSync ?? ((dir, opts) => fs.default.mkdirSync(dir, opts)),
writeFileSync: deps?.writeFileSync ?? ((p, data) => fs.default.writeFileSync(p, data)),
unlinkSync: deps?.unlinkSync ?? (p => fs.default.unlinkSync(p)),
rmSync: deps?.rmSync ?? ((p, opts) => fs.default.rmSync(p, opts)),
execFileSync:
deps?.execFileSync ??
((file, args) =>
cp.default.execFileSync(file, args, {
stdio: ['pipe', 'pipe', 'pipe'],
})),
};

const tarballBase64 = env.GOOGLE_GOG_CONFIG_TARBALL;

if (!tarballBase64) {
// Clean up stale config from a previous run (e.g. after disconnect)
d.rmSync(configDir, { recursive: true, force: true });
delete env.GOG_KEYRING_BACKEND;
delete env.GOG_KEYRING_PASSWORD;
delete env.GOG_ACCOUNT;
return false;
}

// Remove stale config from a previous connection before extracting the new bundle.
// Without this, files present in the old tarball but absent from the new one linger.
d.rmSync(configDir, { recursive: true, force: true });

// Decode tarball and extract to /root/.config/
const parentDir = path.dirname(configDir);
d.mkdirSync(parentDir, { recursive: true });

const tarballBuffer = Buffer.from(tarballBase64, 'base64');

const tmpTarball = path.join(parentDir, 'gogcli-config.tar.gz');
d.writeFileSync(tmpTarball, tarballBuffer);

try {
d.execFileSync('tar', ['xzf', tmpTarball, '-C', parentDir]);
console.log(`[gog] Extracted config tarball to ${configDir}`);
} finally {
try {
d.unlinkSync(tmpTarball);
} catch {
Copy link
Contributor

Choose a reason for hiding this comment

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

WARNING: Temp credential archive cleanup hides real failures

This catch {} ignores permission and I/O errors from unlinkSync(tmpTarball). Because tmpTarball is written under /root/.config on the persistent volume, a failed cleanup leaves a second copy of the user's Google tokens and client secrets on disk after startup. Please only ignore ENOENT here and surface other unlink failures.

// ignore cleanup errors
}
}

// Set env vars for gog runtime.
// GOG_KEYRING_PASSWORD is NOT a secret. The 99designs/keyring file backend
// requires a password to operate, but gog runs inside a single-tenant VM
// with no shared access. The value is arbitrary — it just needs to be
// consistent across setup (google-setup/setup.mjs), container startup
// (start-openclaw.sh), and here.
env.GOG_KEYRING_BACKEND = 'file';
env.GOG_KEYRING_PASSWORD = 'kiloclaw';
if (env.GOOGLE_ACCOUNT_EMAIL) {
env.GOG_ACCOUNT = env.GOOGLE_ACCOUNT_EMAIL;
}

return true;
}
10 changes: 10 additions & 0 deletions kiloclaw/controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { registerHealthRoute } from './routes/health';
import { registerGatewayRoutes } from './routes/gateway';
import { registerConfigRoutes } from './routes/config';
import { CONTROLLER_COMMIT, CONTROLLER_VERSION } from './version';
import { writeGogCredentials } from './gog-credentials';

export type RuntimeConfig = {
port: number;
Expand Down Expand Up @@ -112,6 +113,15 @@ async function handleHttpRequest(

export async function startController(env: NodeJS.ProcessEnv = process.env): Promise<void> {
const config = loadRuntimeConfig(env);

// Write gog credentials before starting the gateway so env vars are available
// to the child process on first spawn. Best-effort: log and continue on failure.
try {
await writeGogCredentials(env as Record<string, string | undefined>);
} catch (err) {
console.error('[gog] Failed to write credentials:', err);
}

const supervisor = createSupervisor({
gatewayArgs: config.gatewayArgs,
});
Expand Down
Loading
Loading