Skip to content
Draft
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
20 changes: 19 additions & 1 deletion devlog/_plan/260912_operations/050_pairing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,28 @@ Class C3; dependency roadmap. Reuse existing connected-client state and browser-

MODIFY owning dashboard pending-auth component and bootstrap state: distinguish a reachable connected machine awaiting hub browser authentication from a stopped standalone proxy. Show configured hub identity/origin, explain that machine enrollment and browser session are separate, offer the current origin-specific existing pairing/authentication action. Preserve revoked/expired/unreachable states and their existing retry actions; do not suggest ocx start while the local runtime is reachable. Derive the next action from current origin + configured hub instead of a hardcoded localhost URL. No credentials appear in visible copy/URLs.

MODIFY all gui/src/i18n locale dictionaries with meaningful labels. Extend existing pending-auth/dashboard tests for local origin, remote hub origin, pending, authenticated, expired/revoked and unavailable standalone; positive browser auth transitions into connected dashboard. Exact files: gui/src/App.tsx, api.ts, pages/dashboard-core-poll.ts, pages/use-dashboard-data.ts and pages/Dashboard.tsx consume a classified authentication/error state instead of a boolean. Existing connect-pairing.ts and connect-pairing-transport.ts own hub identity and origin-specific action. Define the error classification in api.ts at response ingress; consume in polling and Dashboard; reset on authenticated success and pairing completion. No persistence/serialization for this UI state. Keep cached data with stale labeling when auth fails; do not erase a known hub into standalone offline. Public hub/browser-pairing guidance is updated with the same distinction. No service restart or live auth reconfiguration.
MODIFY all gui/src/i18n locale dictionaries with meaningful labels. Extend existing pending-auth/dashboard tests for local origin, remote hub origin, pending, authenticated, expired/revoked and unavailable standalone; positive browser auth transitions into connected dashboard. Exact files: gui/src/App.tsx, api.ts, pages/dashboard-core-poll.ts, pages/use-dashboard-data.ts and pages/Dashboard.tsx consume a classified authentication/error state instead of a boolean. Existing connect-pairing.tsx and connect-pairing-transport.ts own hub identity and origin-specific action. Define the error classification in api.ts at response ingress; consume in polling and Dashboard; reset on authenticated success and pairing completion. No persistence/serialization for this UI state. Keep cached data with stale labeling on non-auth read failures; hide it when authentication or permission is denied; do not erase a known hub into standalone offline. Public hub/browser-pairing guidance is updated with the same distinction. No service restart or live auth reconfiguration.

Hosted component suite and screenshot artifact of the rendered pending state required for final delivery; local GUI tests/build NOT RUN. Static source or mockup is not rendered application evidence.

Accepted OPS-PAIR-01/02. Cases include browser session expiry and post-pairing refresh, local and hub origin guidance, code versus API/admin-key explanation, and operator handoff text. Prefer existing component tests; new test files only where needed.

Reflection amendments: reuse existing api.ts SESSION_UNAVAILABLE_EVENT and App sharedSessionReady; subscribe in App, emit on terminal 401 expiry (not aborted requests), reset/read refresh on successful pairing. Do not create duplicate auth state. Poll classification and pairing errors distinguish HTTP auth refusal, transport/network, and invalid responses; aborted work does not show a failure.

P revalidation at81f0c78d7a: same App, Dashboard, pairing and API owners remain. This independent branch starts from refresheddev; previous usage-transportD directs pairing after the user-requested source repairs.

Concrete delta: App subscribes to SESSION_UNAVAILABLE_EVENT for the shared plane and derives readiness from hasApiSession; ignore a late notice while a newer session is present. Pairing success increments a dashboard refresh epoch and marks ready. Pass connected/authenticationPending into Dashboard. Hide protected dashboard content while authentication is pending; keep known data with a stale notice only on non-auth read failures. In fetchDashboardOverview distinguish 401(auth), 403(denied), other non-OK(request), invalid JSON/shape(invalid), and transport failure(unavailable); aborted polls propagate without publishing an error. Hook exposes failure and overview refresh without a second authentication store. Only standalone transport unavailability may show ocx start; connected/auth/invalid/request failures use relevant copy and retry.

API wrapper emits its existing unavailable event on terminal401 only when the caller is not aborted and no newer session exists. Retain credential refresh/singleflight behavior; no new auth bypass or token persistence. Dashboard receives success epoch as a prop; useDashboardData adds it to existing useKeyedClientResource revalidation dependencies without changing resource keys or remounting. Every dashboard resource refreshes even when a settled failed/cached store survived.

Pairing form shows target.serverOrigin, a copyable ocx gui pair --origin command for window.location.origin, instructions to run it on the hub or ask its operator, and the distinction between one-time code and API/admin keys. Reuse useCopyFeedback and existing copy labels; copy failure remains visible. Keep relay technical copy subordinate. Pairing transport gets a typed error kind (invalid-code/refused/unreachable/invalid-response), mapped to localized actionable copy while preserving pasted code; abort does not publish an error. This is process-local UI state, not a wire schema.

Exact regressions: extend gui/tests/connect-pairing.test.ts for real App dashboard pending/authsuccess/expiry/recovery and hub/command identity; extend api-auth-deadline.test.ts for terminal notice behavior if needed; NEW gui/tests/dashboard-connection-state.test.ts for poll failure classes, cached data and no erroneous start advice. All9 locale modules get new copy. Existing Notice/buttons/tokens, variance2/motion1, dense utility layout; no decorative assets or new dependencies. Hosted built preview, inspected screenshot and browser interaction supply rendered proof later; local suites/build NOT RUN.

Pairing lifetime precision: form keyed by target server/bootstrap identity, one AbortController per submit cancelled on unmount; transport accepts optional caller signal in addition to its existing fetch seam and checks abort before session installation. This prevents an obsolete target response from installing a session or publishing errors after its form unmounts. Keep existing request method/credential mechanics unchanged.

A amendment: post-pairing refresh explicitly reaches each dashboard keyed resource through [apiBase, refreshEpoch] dependencies; a component remount is not treated as a cache invalidation mechanism. Regression first seeds a failed overview store, completes pairing, and requires a new authenticated health/provider read plus rendered data.

Reflection03/05 closure:403 keeps distinct permission-denied guidance and never starts or re-pairs a running proxy merely for denied permissions. Validate HealthData status/version strings and finite nonnegative uptime; providers must be an array of objects with the required name/adapter/baseUrl strings and hasApiKey boolean, optional defaultModel string. Invalid shapes are classified invalid even with HTTP200. Unauthorized/denied content stays hidden; only nonauth read failure may show cached data with stale notice.

Resume C repairs: hosted34682559994 found erasableSyntaxOnly constructor parameter-property and React ref analysis at createElement form. Explicit class field and JSX component preserve behavior without disabling rules. Source compound-failure repair was already published externally at4124a644; local byte-identical patch preserved before fast-forward. Hidden-document App fixture disables periodic polls, and a controlled real resource deadline verifies retained data becomes stale. All local suites/build/typecheck/install NOT RUN.
9 changes: 9 additions & 0 deletions devlog/_plan/260912_operations/110_resume_status.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Operations resume checkpoint

Update carry #4343 merged with exact candidate f37894711158fa8215d26bed642389148ac395f6 and successful Cross-platform CI34674523305. The already-carried stop fix is not replayed. Original issue/PR closure stays with integration coordination.

Listener #4353, usage #4357 → #4373 and pairing #4378 remain open. Published usage source/security audits passed; hosted execution must be checked on the final cumulative tip. Prior Cline registration/native-restore fixture failures are retained as failures; current dev has a separate repair, and this unit does not duplicate its ownership.

Pairing resumed at persisted C. The previous local compound-failure patch matched the newer remote commit byte-for-byte and was preserved before fast-forward. The subsequent fix uses an erasable explicit error field and JSX event handlers for the hosted compiler/lint failures; no checks were disabled. A hidden-document pairing fixture excludes periodic polling, and a controlled resource deadline verifies stale-data marking. Hosted execution and rendered preview remain pending.

The OpenCode management-token and local transport change remains outstanding against original #4317 CHANGES_REQUESTED. No whole-lane completion is claimed. All local suites, focused tests, GUI tests, build, typecheck and installs are NOT RUN. No service changes or release actions were performed.
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -722,3 +722,9 @@ For a service rollback, stop the branch service and repair the prior release aga
session, not a client data key.
- **Outstanding revocation after disconnect:** use the hub dashboard's **Integrations → API Keys**
page. It is the sole post-disconnect revocation path.

### Pairing this browser with a hub

Machine enrollment and browser authentication are separate. The pairing panel names the hub and displays an `ocx gui pair --origin` command for the exact origin currently open in your browser. Run that command on the hub, or send it to the hub operator and request a one-time pairing code. Paste that code into the panel; a data API key or admin token is not a pairing code.

While browser authentication is pending, the dashboard does not recommend restarting a healthy connected client. Completing pairing refreshes the dashboard data immediately, including a previously cached authentication failure. Session expiry returns to pairing; permission denial keeps its own access-settings guidance. Other failed refreshes may show the last received data with a stale-data notice and retry action.
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -338,3 +338,9 @@ Adding **Ollama Cloud** or another catalog provider from the dashboard copies it
classification into the saved provider config, so the [vision sidecar](/guides/sidecars/)
is gated correctly without manual classification.
:::

### Pairing this browser with a hub

Machine enrollment and browser authentication are separate. The pairing panel names the hub and displays an `ocx gui pair --origin` command for the exact origin currently open in your browser. Run that command on the hub, or send it to the hub operator and request a one-time pairing code. Paste that code into the panel; a data API key or admin token is not a pairing code.

While browser authentication is pending, the dashboard does not recommend restarting a healthy connected client. Completing pairing refreshes the dashboard data immediately, including a previously cached authentication failure. Session expiry returns to pairing; permission denial keeps its own access-settings guidance. Other failed refreshes may show the last received data with a stale-data notice and retry action.
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/ko/guides/remote-hub.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,3 +336,9 @@ docker compose up -d
- `/v1/catalog`가 `403 origin_rejected`인데 `/readyz`가 `200`이면 데이터 리스너가 TLS 프런트엔드 뒤에서 루프백에 바인드되어 있습니다. [데이터 리스너에 TLS 붙이기](#데이터-리스너에-tls-붙이기)를 보세요.
- 브라우저 로그아웃/만료는 해당 원격 세션만 끊습니다. 데이터 키와는 별개입니다.
- 연결 해제 후 남은 키는 허브의 **Integrations → API Keys**에서만 폐기할 수 있습니다.

### 이 브라우저를 허브에 인증하기

기기 연결과 브라우저 인증은 별개입니다. 페어링 패널에 표시된 허브에서 현재 브라우저 주소용 `ocx gui pair --origin` 명령을 실행하세요. 직접 운영하지 않는 허브라면 운영자에게 명령을 전달하고 일회용 코드를 요청하세요. 입력 칸에는 페어링 코드를 붙여 넣습니다. 데이터 API 키나 관리자 토큰을 대신 입력하지 마세요.

인증을 기다리는 동안 정상인 클라이언트를 재시작하라고 안내하지 않습니다. 페어링을 마치면 이전 인증 오류가 캐시에 남아 있어도 대시보드를 새로 읽습니다. 세션이 만료되면 페어링 화면으로 돌아가며, 권한 거부는 별도로 안내합니다. 다른 갱신 오류에서는 마지막 데이터를 오래된 정보로 표시하고 재시도할 수 있습니다.
6 changes: 6 additions & 0 deletions docs-site/src/content/docs/ko/guides/web-dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,3 +229,9 @@ GUI에서 OAuth 계정을 선택하면 풀 모드에서도 다음 요청에 반
이유만으로 바꾸지 않아요. 선택 계정이 429를 반환하면 풀이 꺼져 있어도 사용 가능한 다른
계정으로 자동 전환해요. 자동 선택이 저장되면 GUI의 활성 표시도 즉시 바뀌어요.
이미 서버로 보낸 요청의 인증 정보는 바꾸지 않아요.

### 이 브라우저를 허브에 인증하기

기기 연결과 브라우저 인증은 별개입니다. 페어링 패널에 표시된 허브에서 현재 브라우저 주소용 `ocx gui pair --origin` 명령을 실행하세요. 직접 운영하지 않는 허브라면 운영자에게 명령을 전달하고 일회용 코드를 요청하세요. 입력 칸에는 페어링 코드를 붙여 넣습니다. 데이터 API 키나 관리자 토큰을 대신 입력하지 마세요.

인증을 기다리는 동안 정상인 클라이언트를 재시작하라고 안내하지 않습니다. 페어링을 마치면 이전 인증 오류가 캐시에 남아 있어도 대시보드를 새로 읽습니다. 세션이 만료되면 페어링 화면으로 돌아가며, 권한 거부는 별도로 안내합니다. 다른 갱신 오류에서는 마지막 데이터를 오래된 정보로 표시하고 재시도할 수 있습니다.
21 changes: 18 additions & 3 deletions gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { SidebarGithubRow } from "./components/sidebar-github-row";
import { IconGrid, IconServer, IconBoxes, IconBot, IconList, IconActivity, IconHardDrive, IconCodex, IconMenu, IconSun, IconMoon, IconMonitor, IconGlobe, IconPower, IconX, IconRefresh} from "./icons";
import { useI18n, useT, LOCALES, localeDisplayName, type Locale, type TKey } from "./i18n/shared";
import { Select } from "./ui";
import { configureApiTargets, hasApiSession, installApiAuthFetch, installApiSessionFromHtml, logoutApiSession } from "./api";
import { configureApiTargets, hasApiSession, installApiAuthFetch, installApiSessionFromHtml, logoutApiSession, SESSION_UNAVAILABLE_EVENT } from "./api";
import { apiBaseForPlane, discoverApiTargets, isConnectedRuntime, standaloneApiTargets, type ApiTargets } from "./api-targets";
import { ConnectPairingForm } from "./connect-pairing";
import { type Page } from "./app-routing";
Expand Down Expand Up @@ -111,8 +111,19 @@ export default function App() {
const [targetsSettled, setTargetsSettled] = useState(() => !isConnectedRuntime());
const [targetError, setTargetError] = useState(false);
const [sharedSessionReady, setSharedSessionReady] = useState(() => hasApiSession("shared"));
const [sharedSessionEpoch, setSharedSessionEpoch] = useState(0);
const [sessionLoggingOut, setSessionLoggingOut] = useState(false);

useEffect(() => {
const unavailable = (event: Event) => {
if ((event as CustomEvent<{ plane?: string }>).detail?.plane === "shared" && !hasApiSession("shared")) {
setSharedSessionReady(false);
}
};
window.addEventListener(SESSION_UNAVAILABLE_EVENT, unavailable);
return () => window.removeEventListener(SESSION_UNAVAILABLE_EVENT, unavailable);
}, []);

useEffect(() => {
const controller = new AbortController();
void discoverApiTargets(API_BASE, controller.signal).then(async next => {
Expand Down Expand Up @@ -422,9 +433,13 @@ export default function App() {
<div className="alert alert-err" role="alert">{t("connection.machineUnavailable")}</div>
)}
{targets.connected && !sharedSessionReady && (
<ConnectPairingForm target={targets.shared} onConnected={() => setSharedSessionReady(true)} />
<ConnectPairingForm key={`${targets.shared.serverOrigin}:${targets.shared.bootstrapPath}`} target={targets.shared} onConnected={() => {
setSharedSessionReady(true);
setSharedSessionEpoch(epoch => epoch + 1);
}} />
)}
{page === "dashboard" && <Dashboard apiBase={sharedBase} />}
{page === "dashboard" && <Dashboard apiBase={sharedBase} connected={targets.connected}
authenticationPending={targets.connected && !sharedSessionReady} refreshEpoch={sharedSessionEpoch} />}
{page === "startup" && <Startup apiBase={sharedBase} machineApiBase={machineBase} connected={targets.connected} />}
{page === "providers" && <Providers apiBase={sharedBase} />}
{page === "models" && <Models key={sharedBase} apiBase={sharedBase} restartEpoch={codexRestartEpoch} />}
Expand Down
16 changes: 10 additions & 6 deletions gui/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { createBoundedFetch } from "./bounded-fetch";
import { adminTokenPromptAllowed, standaloneApiTargets, type ApiPlane, type ApiTarget, type ApiTargets } from "./api-targets";

/**
* Fired instead of the admin-token prompt when the dashboard cannot start a session on a
* deployment that has no admin token to type. The shell renders it as a notice; nothing
* blocks on it.
* Fired after an unauthorized request cannot recover a session. The shell synchronizes
* its existing readiness state; cancelled callers and newer valid sessions emit no notice.
*/
export const SESSION_UNAVAILABLE_EVENT = "opencodex:session-unavailable";

Expand Down Expand Up @@ -282,7 +281,6 @@ async function resolveTokenAfter401(plane: ApiPlane, failedToken: string | null,
// of a password box the user cannot answer (#3353, #3483).
if (!adminTokenPromptAllowed()) {
state.promptCancelled = true;
reportSessionUnavailable(plane);
return null;
}
const prompted = await requestAdminToken(token => verifyAdminToken(plane, token));
Expand Down Expand Up @@ -332,10 +330,16 @@ export function installApiAuthFetch(): void {
} else clearSessionIfCurrent(classified.plane, token);
const callerSignal = init?.signal ?? (input instanceof Request ? input.signal : undefined);
const nextToken = await resolveTokenAfter401(classified.plane, token, callerSignal ?? undefined);
if (!nextToken) return response;
if (!nextToken) {
if (!callerSignal?.aborted && !hasApiSession(classified.plane)) reportSessionUnavailable(classified.plane);
return response;
}
const [retryInput, retryInit] = withAuth(classified.plane, input, init, nextToken);
const retry = await originalFetch(retryInput, retryInit);
if (retry.status === 401) clearSessionIfCurrent(classified.plane, nextToken);
if (retry.status === 401) {
clearSessionIfCurrent(classified.plane, nextToken);
if (!callerSignal?.aborted && !hasApiSession(classified.plane)) reportSessionUnavailable(classified.plane);
}
return retry;
};
}
Expand Down
Loading
Loading