From 3b6e2787c741a09374b9457dcaf0c6af197bb5d0 Mon Sep 17 00:00:00 2001 From: chyoo Date: Thu, 6 Aug 2026 16:04:01 +0900 Subject: [PATCH 1/2] feat: display platform agent status and version --- src/main/index.ts | 60 +++++++++++++++++----------- src/preload/index.ts | 1 + src/renderer/src/env.d.ts | 4 ++ src/renderer/src/pages/Dashboard.tsx | 31 +++++++++++++- 4 files changed, 71 insertions(+), 25 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index 15e98b6..611b026 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -512,6 +512,38 @@ function tailscaleCommandEnv(loginServer: string): NodeJS.ProcessEnv | undefined return /^http:\/\//i.test(loginServer.trim()) ? { TS_DEBUG_ALLOW_HTTP: '1' } : undefined } +function getLauncherOsUser(): string { + const user = (process.env.USER ?? process.env.LOGNAME ?? '').trim() + return /^[a-z_][a-z0-9_-]*\$?$/i.test(user) ? user : '' +} + +async function ensureTailscaleOperator(): Promise { + const operator = getLauncherOsUser() + if (!operator) { + return { ok: false, message: '현재 OS 사용자를 확인하지 못했습니다.' } + } + + const args = ['set', `--operator=${operator}`] + try { + await runCommand('tailscale', args, 15000) + return { ok: true, message: 'Tailscale 제어 권한이 준비되었습니다.' } + } catch (error) { + if (!isTailscalePermissionError(error)) { + return { ok: false, message: commandErrorMessage(error, 'Tailscale 제어 권한 설정 실패') } + } + + try { + await runCommand('pkexec', ['/usr/bin/tailscale', ...args], 30000) + return { ok: true, message: 'Tailscale 제어 권한이 준비되었습니다.' } + } catch (pkexecError) { + return { + ok: false, + message: commandErrorMessage(pkexecError, commandErrorMessage(error, 'Tailscale 제어 권한 설정 실패')), + } + } + } +} + async function installTailscale(): Promise { const installScript = [ 'set -e', @@ -590,13 +622,6 @@ async function resetTailscaleSession(commandEnv?: NodeJS.ProcessEnv): Promise { - const loggedOut = await runCommandIfPossible('pkexec', ['/usr/bin/tailscale', 'logout'], 30000, commandEnv) - if (!loggedOut) { - await runCommandIfPossible('pkexec', ['/usr/bin/tailscale', 'down'], 30000, commandEnv) - } -} - async function readTailscaleStatus(): Promise { const attempts: Array<[string, string[]]> = [ ['tailscale', ['status', '--json']], @@ -714,25 +739,11 @@ async function loginTailscale( await waitForTailscaleConnected() return { ok: true, message: 'Tailscale 연결 완료' } } catch (error) { - if (isTailscalePermissionError(error)) { - try { - if (forceReset) { - await resetTailscaleSessionWithPkexec(commandEnv) - } - await runCommand('pkexec', ['/usr/bin/tailscale', ...tailscaleArgs], 60000, commandEnv) - await waitForTailscaleConnected() - return { ok: true, message: 'Tailscale 연결 완료' } - } catch (pkexecError) { - return { - ok: false, - message: commandErrorMessage(pkexecError, commandErrorMessage(error, 'Tailscale 제어 권한이 없습니다.')), - } - } - } - return { ok: false, - message: commandErrorMessage(error, 'Tailscale 연결 실패'), + message: isTailscalePermissionError(error) + ? commandErrorMessage(error, 'Tailscale 제어 권한이 없습니다. 다시 연결해 주세요.') + : commandErrorMessage(error, 'Tailscale 연결 실패'), } } } @@ -1027,6 +1038,7 @@ function createWindow(): void { }) ipcMain.handle('launcher-tailscale-status', () => readTailscaleStatus()) ipcMain.handle('launcher-tailscale-install', () => installTailscale()) + ipcMain.handle('launcher-tailscale-ensure-operator', () => ensureTailscaleOperator()) ipcMain.handle('launcher-ssh-key-ensure', () => ensureLauncherSshKey()) ipcMain.handle( 'launcher-tailscale-login', diff --git a/src/preload/index.ts b/src/preload/index.ts index dfdf753..5d9dfa1 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -14,6 +14,7 @@ const appWindowMeta = { isAppWindow, appName, url: appUrl } const launcherTailscale = { status: () => ipcRenderer.invoke('launcher-tailscale-status'), install: () => ipcRenderer.invoke('launcher-tailscale-install'), + ensureOperator: () => ipcRenderer.invoke('launcher-tailscale-ensure-operator'), login: (authKey: string, loginServer: string, hostname?: string, forceReset?: boolean) => ipcRenderer.invoke('launcher-tailscale-login', authKey, loginServer, hostname, forceReset), logout: () => ipcRenderer.invoke('launcher-tailscale-logout'), diff --git a/src/renderer/src/env.d.ts b/src/renderer/src/env.d.ts index d0b1403..ef8e636 100644 --- a/src/renderer/src/env.d.ts +++ b/src/renderer/src/env.d.ts @@ -40,6 +40,10 @@ declare global { ok: boolean message: string }> + ensureOperator: () => Promise<{ + ok: boolean + message: string + }> login: (authKey: string, loginServer: string, hostname?: string, forceReset?: boolean) => Promise<{ ok: boolean message: string diff --git a/src/renderer/src/pages/Dashboard.tsx b/src/renderer/src/pages/Dashboard.tsx index e03890a..8e73707 100644 --- a/src/renderer/src/pages/Dashboard.tsx +++ b/src/renderer/src/pages/Dashboard.tsx @@ -24,6 +24,7 @@ const LMS_WEB_BASE_URL = `${( ).replace(/\/+$/, '')}/` const NOTICE_READ_STORAGE_KEY = 'atnmy.notice.readSignatures' const LAST_LAUNCHED_APP_STORAGE_KEY = 'atnmy.launcher.lastLaunchedApp' +const TAILSCALE_DEVICE_ID_STORAGE_KEY = 'atnmy.launcher.tailscale-device-id' const IS_DEV = import.meta.env.DEV const TAILSCALE_BACKGROUND_TIMEOUT_MS = 20_000 @@ -102,6 +103,21 @@ function isSameTailscaleAccount(statusAccount: string | undefined, loginAccount: return normalizeHeadscaleAccount(statusAccount) === normalizeHeadscaleAccount(loginAccount) } +function getTailscaleDeviceId(): string { + try { + const stored = window.localStorage.getItem(TAILSCALE_DEVICE_ID_STORAGE_KEY)?.trim() + if (stored && /^[a-z0-9-]{6,24}$/i.test(stored)) return stored + + const next = typeof crypto !== 'undefined' && 'randomUUID' in crypto + ? crypto.randomUUID().replace(/-/g, '').slice(0, 10) + : `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}` + window.localStorage.setItem(TAILSCALE_DEVICE_ID_STORAGE_KEY, next) + return next + } catch { + return 'launcher-client' + } +} + function isTailscaleMissingStatus(status: Awaited['status']>> | null): boolean { return Boolean( status?.backendState === 'Unavailable' && @@ -272,11 +288,24 @@ function Dashboard({ authSession, onSessionUpdate, onLogout, onSessionExpired }: const sshKey = await window.launcherSshKey?.ensure().catch(() => null) if (cancelled) return + const operator = await launcherTailscale.ensureOperator() + if (cancelled) return + if (!operator.ok) { + setTailscaleIssue({ + state: 'warning', + title: '원격 연결 권한 필요', + message: operator.message || 'Tailscale 제어 권한을 설정하지 못했습니다.', + }) + return + } + const response = await fetchWithAuthRefreshRef.current(`${LAUNCHER_API_BASE_URL}/api/provision/session`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - hostname: window.navigator.userAgent.includes('Electron') ? 'launcher-client' : 'client', + hostname: window.navigator.userAgent.includes('Electron') + ? `launcher-client-${getTailscaleDeviceId()}` + : `client-${getTailscaleDeviceId()}`, usageType: 'launcher-client', publicSshKey: sshKey?.publicKey, }), From 5099ca74e0f44409d1eac2c3431a4b93e61b22b9 Mon Sep 17 00:00:00 2001 From: chyoo Date: Thu, 6 Aug 2026 16:04:41 +0900 Subject: [PATCH 2/2] version up! --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 79d3137..a2eebf7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "autonomy-studio-launcher", - "version": "1.1.0", + "version": "1.1.1", "description": "Autonomy Studio v2 Frontend", "main": "./out/main/index.js", "author": {