fix(managed-runtime): secure auth and lifecycle control across runtimes (#437)
* feat: add OpenCode server authentication with auto-generated passwords * fix(auth): separate user env and managed OpenCode password state * fix(auth): enforce env precedence and managed password rotation across runtimes * fix(vscode): rotate managed auth on startup and harden webview proxy * build: add dev icons and config for Tauri desktop development * fix(runtime): start managed OpenCode via CLI and expose active API port * fix(managed-runtime): control OpenCode lifecycle and surface secure diagnostics * docs: remove VS Code plugin test runbook
This commit is contained in:
@@ -91,6 +91,19 @@ export const VSCodeLayout: React.FC = () => {
|
||||
return sessions.find((session) => session.id === currentSessionId)?.title || 'Session';
|
||||
}, [currentSessionId, sessions]);
|
||||
const newSessionDraftOpen = useSessionStore((state) => Boolean(state.newSessionDraft?.open));
|
||||
const isSyncingMessages = useSessionStore((state) => state.isSyncing);
|
||||
const hasActiveSessionWork = useSessionStore((state) => {
|
||||
const statuses = state.sessionStatus;
|
||||
if (!statuses || statuses.size === 0) {
|
||||
return false;
|
||||
}
|
||||
for (const status of statuses.values()) {
|
||||
if (status?.type === 'busy' || status?.type === 'retry') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const openNewSessionDraft = useSessionStore((state) => state.openNewSessionDraft);
|
||||
const [connectionStatus, setConnectionStatus] = React.useState<'connecting' | 'connected' | 'error' | 'disconnected'>(
|
||||
() => (typeof window !== 'undefined'
|
||||
@@ -129,10 +142,37 @@ export const VSCodeLayout: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentSessionId && !newSessionDraftOpen && currentView === 'chat') {
|
||||
setCurrentView('sessions');
|
||||
if (currentView !== 'chat') {
|
||||
return;
|
||||
}
|
||||
}, [currentSessionId, newSessionDraftOpen, currentView, viewMode]);
|
||||
|
||||
if (currentSessionId || newSessionDraftOpen || isSyncingMessages || hasActiveSessionWork) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeoutId = window.setTimeout(() => {
|
||||
const state = useSessionStore.getState();
|
||||
const stillNoSession = !state.currentSessionId;
|
||||
const draftStillClosed = !state.newSessionDraft?.open;
|
||||
const stillSyncing = state.isSyncing;
|
||||
const stillActiveWork = (() => {
|
||||
const statuses = state.sessionStatus;
|
||||
if (!statuses || statuses.size === 0) return false;
|
||||
for (const status of statuses.values()) {
|
||||
if (status?.type === 'busy' || status?.type === 'retry') return true;
|
||||
}
|
||||
return false;
|
||||
})();
|
||||
|
||||
if (stillNoSession && draftStillClosed && !stillSyncing && !stillActiveWork) {
|
||||
setCurrentView('sessions');
|
||||
}
|
||||
}, 900);
|
||||
|
||||
return () => {
|
||||
window.clearTimeout(timeoutId);
|
||||
};
|
||||
}, [currentSessionId, newSessionDraftOpen, currentView, viewMode, isSyncingMessages, hasActiveSessionWork]);
|
||||
|
||||
const handleBackToSessions = React.useCallback(() => {
|
||||
setCurrentView('sessions');
|
||||
|
||||
@@ -486,7 +486,7 @@ export const useEventStream = () => {
|
||||
// Note: needs_attention logic is now handled by the server
|
||||
// Server maintains authoritative state based on view tracking and message events
|
||||
|
||||
if (prevType !== nextType) {
|
||||
if (process.env.NODE_ENV === 'development' && prevType !== nextType) {
|
||||
try {
|
||||
console.info('[SESSION-STATUS]', {
|
||||
sessionId,
|
||||
|
||||
@@ -264,11 +264,29 @@ export const debugUtils = {
|
||||
const resp = await fetch('/api/health');
|
||||
const contentType = resp.headers.get('content-type') || '';
|
||||
const body = await safeText(resp);
|
||||
const isJson = contentType.toLowerCase().includes('application/json');
|
||||
let parsed: Record<string, unknown> | null = null;
|
||||
if (isJson && body) {
|
||||
try {
|
||||
const candidate = JSON.parse(body) as unknown;
|
||||
if (candidate && typeof candidate === 'object' && !Array.isArray(candidate)) {
|
||||
parsed = candidate as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
parsed = null;
|
||||
}
|
||||
}
|
||||
opencodeHealth = {
|
||||
status: resp.status,
|
||||
ok: resp.ok,
|
||||
contentType,
|
||||
type: contentType.includes('application/json') ? 'json' : 'html',
|
||||
type: isJson ? 'json' : 'html',
|
||||
openCodePort: parsed?.openCodePort ?? null,
|
||||
openCodeRunning: parsed?.openCodeRunning ?? null,
|
||||
openCodeSecureConnection: parsed?.openCodeSecureConnection ?? null,
|
||||
openCodeAuthSource: parsed?.openCodeAuthSource ?? null,
|
||||
isOpenCodeReady: parsed?.isOpenCodeReady ?? null,
|
||||
lastOpenCodeError: parsed?.lastOpenCodeError ?? null,
|
||||
preview: body ? body.slice(0, 120) : null,
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -13,6 +13,8 @@ type ProbeResult = {
|
||||
type OpenChamberHealthSnapshot = {
|
||||
openCodePort?: unknown;
|
||||
openCodeRunning?: unknown;
|
||||
openCodeSecureConnection?: unknown;
|
||||
openCodeAuthSource?: unknown;
|
||||
isOpenCodeReady?: unknown;
|
||||
lastOpenCodeError?: unknown;
|
||||
opencodeBinaryResolved?: unknown;
|
||||
@@ -100,6 +102,19 @@ const formatIso = (timestamp: number | null | undefined): string => {
|
||||
}
|
||||
};
|
||||
|
||||
const normalizePort = (value: unknown): number | null => {
|
||||
if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
|
||||
return Math.trunc(value);
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number.parseInt(value, 10);
|
||||
if (Number.isFinite(parsed) && parsed > 0) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const buildOpenCodeStatusReport = async (): Promise<string> => {
|
||||
const now = new Date();
|
||||
const appVersion = typeof __APP_VERSION__ !== 'undefined' ? __APP_VERSION__ : '(unknown)';
|
||||
@@ -215,6 +230,18 @@ export const buildOpenCodeStatusReport = async (): Promise<string> => {
|
||||
lines.push(`Directory: ${directory || '(none)'}`);
|
||||
lines.push(`Platform: ${platform}`);
|
||||
|
||||
const runtimeOpenCodePort = normalizePort(openChamberHealth?.openCodePort);
|
||||
lines.push(`OpenCode runtime port: ${runtimeOpenCodePort ?? '(unknown)'}`);
|
||||
if (typeof openChamberHealth?.openCodeRunning === 'boolean') {
|
||||
lines.push(`OpenCode runtime running: ${openChamberHealth.openCodeRunning ? 'yes' : 'no'}`);
|
||||
}
|
||||
if (typeof openChamberHealth?.openCodeSecureConnection === 'boolean') {
|
||||
lines.push(`Secure OpenCode connection: ${openChamberHealth.openCodeSecureConnection ? 'true' : 'false'}`);
|
||||
}
|
||||
if (typeof openChamberHealth?.openCodeAuthSource === 'string' && openChamberHealth.openCodeAuthSource.trim()) {
|
||||
lines.push(`OpenCode auth source: ${openChamberHealth.openCodeAuthSource}`);
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const injected = (window as unknown as { __OPENCHAMBER_MACOS_MAJOR__?: unknown }).__OPENCHAMBER_MACOS_MAJOR__;
|
||||
if (typeof injected === 'number' && Number.isFinite(injected) && injected > 0) {
|
||||
|
||||
@@ -1707,12 +1707,14 @@ export const useMessageStore = create<MessageStore>()(
|
||||
};
|
||||
|
||||
if (messageIndex === -1) {
|
||||
console.info("[MESSAGE-DEBUG] updateMessageInfo: messageIndex === -1", {
|
||||
sessionId,
|
||||
messageId,
|
||||
messageInfo,
|
||||
existingCount: normalizedSessionMessages.length,
|
||||
});
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.info("[MESSAGE-DEBUG] updateMessageInfo: messageIndex === -1", {
|
||||
sessionId,
|
||||
messageId,
|
||||
messageInfo,
|
||||
existingCount: normalizedSessionMessages.length,
|
||||
});
|
||||
}
|
||||
|
||||
if (normalizedSessionMessages.length > 0) {
|
||||
const firstMessage = normalizedSessionMessages[0];
|
||||
|
||||
Reference in New Issue
Block a user