Skip to content
Closed
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
11 changes: 7 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "opencodex-desktop"
version = "2.92.0"
version = "2.93.0"
description = "OpenCodex desktop shell"
authors = ["OpenCodex contributors"]
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "OpenCodex",
"version": "2.92.0",
"version": "2.93.0",
"identifier": "com.opencodex.desktop",
"build": {
"frontendDist": "../ui",
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/guides/remote-workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,8 @@ to another computer or folder silently. If the Executor disconnects, the session

Prompt submission acknowledges acceptance immediately; the dashboard polls the session for progress and completion. If the acknowledgement is lost, the draft remains visible with an unknown-submission notice. Check session progress before sending it again; the dashboard never retries a prompt automatically.

If the Pi process exits or its extension fails while a turn is starting, the session reports the failure even if Pi has not acknowledged that turn yet.

**Stop** remains available while a prompt is running. It interrupts the Hub coding-agent turn,
cancels an active Executor command, and prevents a late response from reopening the stopped
session.
Expand Down
2 changes: 2 additions & 0 deletions docs-site/src/content/docs/ko/guides/remote-workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@ ocx remote-workspace status

프롬프트를 보내면 수락 여부를 즉시 응답하고, 대시보드는 진행과 완료 상태를 폴링합니다. 수락 응답을 잃으면 초안은 제출 여부를 알 수 없다는 알림과 함께 남습니다. 다시 보내기 전에 세션 진행 상태를 확인하세요. 대시보드는 프롬프트를 자동 재시도하지 않습니다.

턴을 시작하는 동안 Pi 프로세스가 종료되거나 확장 기능에서 오류가 발생하면, Pi가 아직 해당 턴의 수락을 응답하지 않았더라도 세션에 실패가 표시됩니다.

프롬프트 실행 중에도 **Stop**을 사용할 수 있습니다. Hub 코딩 에이전트의 현재 턴을 중단하고 활성 Executor 명령을 취소하며, 늦게 온 응답이 중지된 세션을 다시 열지 못하게 합니다.

## 여러 세션을 분할해서 보기
Expand Down
4 changes: 2 additions & 2 deletions gui/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion gui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.0.5",
"happy-dom": "20.11.2",
"happy-dom": "21.0.4",
"oxlint": "1.78.0",
"react-refresh": "^0.18.0",
"typescript": "~6.0.2",
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@bitkyc08/opencodex",
"version": "2.92.0",
"version": "2.93.0",
"description": "Universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
"type": "module",
"main": "./bin/package-main.mjs",
Expand Down Expand Up @@ -83,7 +83,7 @@
"@modelcontextprotocol/sdk": "^1.32.1",
"@napi-rs/keyring": "1.3.0",
"bun": "1.4.4",
"zod": "4.4.3"
"zod": "4.6.6"
},
"devDependencies": {
"@types/bun": "1.4.0",
Expand All @@ -94,6 +94,7 @@
},
"overrides": {
"@hono/node-server": "2.1.0",
"body-parser": "2.4.0",
"fast-uri": "^3.1.7",
"hono": "4.13.8",
"ip-address": "^10.4.0",
Expand Down
58 changes: 46 additions & 12 deletions src/codex/desktop-compatibility/windows-certificate-trust.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ export type DesktopCertificateTrust = "trusted" | "not-trusted" | "unknown";
export type TrustOperation = "inspect" | "trust" | "remove";
export type CertificateTrustRunner = (operation: TrustOperation, certificateDer: string, fingerprint: string) => Promise<DesktopCertificateTrust>;

/** Public status is an interactive read; a store that cannot answer within it reports `unknown`. */
export const TRUST_INSPECT_DEADLINE_MS = 10_000;
/** A mutation may wait on the user's certificate prompt. */
export const TRUST_CHANGE_DEADLINE_MS = 120_000;

/** The slice of `child_process.execFile` the runner uses, so a deadline breach can be driven without a real store. */
export type TrustCommandExec = (
file: string,
args: string[],
options: { timeout: number; maxBuffer: number; windowsHide: boolean; encoding: "utf8" },
callback: (error: Error | null, stdout: string) => void,
) => { stdin?: { on(event: "error", listener: () => void): unknown; end(chunk: string): unknown } | null };

export type PowerShellTrustRunnerOptions = {
inspectDeadlineMs?: number;
platform?: NodeJS.Platform;
exec?: TrustCommandExec;
resolvePowerShell?: () => string;
};

const SCRIPT = [
"$ErrorActionPreference='Stop'",
"$inputData=[Console]::In.ReadToEnd()|ConvertFrom-Json",
Expand All @@ -31,18 +51,32 @@ const SCRIPT = [
"}finally{$store.Close();$store.Dispose();$cert.Dispose()}",
].join("\n");

const run: CertificateTrustRunner = (operation, certificateDer, fingerprint) => {
if (process.platform !== "win32") return Promise.resolve("unknown");
if (operation !== "inspect" && isTestHomeGuardArmed()) return Promise.resolve("unknown");
return new Promise(resolve => {
const child = execFile(resolveTrustedWindowsPowerShellExe(), ["-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
timeout: operation === "inspect" ? 10_000 : 120_000, maxBuffer: 4096, windowsHide: true, encoding: "utf8",
}, (error, stdout) => resolve(!error && (stdout.trim() === "trusted" || stdout.trim() === "not-trusted")
? stdout.trim() as DesktopCertificateTrust : "unknown"));
child.stdin?.on("error", () => { /* completion and readback decide the result */ });
child.stdin?.end(JSON.stringify({ operation, certificate: certificateDer, fingerprint }));
});
};
/**
* The real CurrentUser Root runner. Any failure, including a deadline breach, is `unknown`, which
* never authorizes a write. The options exist so a test can replay this exact call with a cold-start
* deadline and can drive a breach deterministically; production uses the defaults.
*/
export function createPowerShellCertificateTrustRunner({
inspectDeadlineMs = TRUST_INSPECT_DEADLINE_MS,
platform = process.platform,
exec = execFile as unknown as TrustCommandExec,
resolvePowerShell = resolveTrustedWindowsPowerShellExe,
}: PowerShellTrustRunnerOptions = {}): CertificateTrustRunner {
return (operation, certificateDer, fingerprint) => {
if (platform !== "win32") return Promise.resolve("unknown");
if (operation !== "inspect" && isTestHomeGuardArmed()) return Promise.resolve("unknown");
return new Promise(resolve => {
const child = exec(resolvePowerShell(), ["-NoProfile", "-NonInteractive", "-Command", SCRIPT], {
timeout: operation === "inspect" ? inspectDeadlineMs : TRUST_CHANGE_DEADLINE_MS, maxBuffer: 4096, windowsHide: true, encoding: "utf8",
}, (error, stdout) => resolve(!error && (stdout.trim() === "trusted" || stdout.trim() === "not-trusted")
? stdout.trim() as DesktopCertificateTrust : "unknown"));
child.stdin?.on("error", () => { /* completion and readback decide the result */ });
child.stdin?.end(JSON.stringify({ operation, certificate: certificateDer, fingerprint }));
});
};
}

const run = createPowerShellCertificateTrustRunner();

/** Public status cannot create a certificate, decrypt a key or alter OS trust. */
export async function inspectWindowsCertificateTrust(certPem: string, expectedFingerprint: string, runner: CertificateTrustRunner = run): Promise<DesktopCertificateTrust> {
Expand Down
28 changes: 27 additions & 1 deletion src/codex/native-main-owner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { join } from "node:path";
import { isAbsolute, join, relative, resolve, sep } from "node:path";

import { Database } from "bun:sqlite";

Expand Down Expand Up @@ -299,6 +299,32 @@ export function nativeMainOwnerSnapshot(context: NativeProfileContext): NativeMa
return entry ? { ...entry.snapshot } : null;
}

/**
* Homes at or below `root` whose owner entry may still hold or reopen handles there.
*
* Conservative on purpose: besides a published database or side descriptor, an
* acquisition in flight (`drive`) holds its descriptor and database in locals until
* hardening returns, a pending retry timer will reopen them, and a detached entry is
* still closing. Removal code asks this before deleting a home. Windows refuses to
* delete a file SQLite holds open, so a removal that starts early fails with EBUSY
* only after its whole retry budget, without naming the owner.
*/
export function nativeMainOwnerHomesOpenAtOrBelow(root: string): string[] {
const fold = (path: string) => (process.platform === "win32" ? resolve(path).toLowerCase() : resolve(path));
const base = fold(root);
const open: string[] = [];
const candidates = [
...[...entries.values()].filter(entry => entry.database || entry.file || entry.drive || entry.timer),
...detachedEntries,
];
for (const entry of candidates) {
const suffix = relative(base, fold(entry.context.codexHome));
if (suffix === ".." || suffix.startsWith(`..${sep}`) || isAbsolute(suffix)) continue;
open.push(entry.context.codexHome);
}
return open;
}

export function assertNativeMainOwner(context: NativeProfileContext): void {
const entry = entries.get(context.homeId);
if (entry?.snapshot.status === "held" && entry.database && !entry.closing) return;
Expand Down
17 changes: 11 additions & 6 deletions src/remote-control/workspace-pi-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,18 @@ class PiRpcProcess {
async prompt(message: string): Promise<void> {
if (this.activeSettle) throw new Error("Pi Remote Workspace turn is already active");
const settled = new Promise<void>((resolve, reject) => { this.activeSettle = { resolve, reject }; });
const active = this.activeSettle;
try {
const accepted = await this.command("prompt", { message });
if (accepted.success !== true) throw new Error(safeError(accepted.error, "Pi rejected the prompt"));
await settled;
} catch (error) {
this.activeSettle = null;
throw error;
// Either operation can fail first. Observe both immediately, including an
// extension error or child exit that arrives before the prompt response.
await Promise.all([
this.command("prompt", { message }).then(accepted => {
if (accepted.success !== true) throw new Error(safeError(accepted.error, "Pi rejected the prompt"));
}),
settled,
]);
} finally {
if (this.activeSettle === active) this.activeSettle = null;
}
}

Expand Down
15 changes: 10 additions & 5 deletions structure/ops/test-sandbox-cleanup.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,16 @@ fixture-specific cleanup or claim OS ACL coverage for synthetic tests.
case roots (`tests/codex-integration/codex-quota-auto-refresh.test.ts`), and Anthropic instance
root removal (`tests/helpers/anthropic-instance-fixture.ts`).
It settles producers, then each root's config flights, closes the request-history SQLite index,
drains each root's ACL work, and only then removes drained roots. A producer or history failure
blocks every removal; a root-specific drain failure preserves that root while other drained roots
can be removed. Removal failures do not stop later roots. Environment restoration runs in finally,
and the first failure is rethrown. Undrained roots remain for run-level sandbox cleanup.
`tests/ci-workflows/fixture-teardown-helper.test.ts` verifies ordering and failure gating with injected owners.
drains each root's ACL work, then asks `nativeMainOwnerHomesOpenAtOrBelow` whether a native-main
owner below the root is still open, acquiring (descriptor and SQLite handle held while its hardener
runs), waiting on a retry, or closing; only roots that pass both are removed. An open owner fails
that root at once with an error naming the owner home, without spending the removal retry budget.
A producer or history failure blocks every removal; a root-specific drain or owner failure
preserves that root while other ready roots can be removed. Removal failures do not stop later
roots. Environment restoration runs in finally, and the first failure is rethrown. Undrained roots
remain for run-level sandbox cleanup.
`tests/ci-workflows/fixture-teardown-helper.test.ts` verifies ordering and failure gating with injected
owners, and with a real owner whose hardener is paused mid-acquisition.

`tests/helpers/spawn-test-child.ts` owns behavior and warm-up children on the parent's live
event loop. It captures output continuously, kills at the child deadline, and bounds exit/pipe
Expand Down
2 changes: 2 additions & 0 deletions structure/remote-workspace.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ RPC v1 peers fail closed and must upgrade together rather than fall back to imme

`src/remote-control/workspace-runtime.ts` is the lazy composition owner for Hub services. Codex, Claude and Pi adapters keep model processes on the Hub and expose selected remote tools. Their source configuration is not evidence of live CLI confinement. `src/cli/remote-workspace.ts` contains registered executor pair/agent/status handling; the machine-readable entries live in `src/cli/capabilities.ts`.

Pi prompt admission and turn settlement are observed concurrently in `src/remote-control/workspace-pi-runtime.ts`. Either failure rejects the prompt; success requires a successful prompt response and the turn's settlement event. An extension error or child exit before admission cannot leave an unobserved rejection. `tests/clients/remote-workspace-cli-runtimes.test.ts` covers both event orders and failure paths with isolated RPC children; this is not live Pi confinement evidence.

The explicit hub status/runtimes/sessions branch selects `src/cli/remote-workspace-hub.ts` before executor storage. It performs fixed management reads and projects public devices/root labels, named runtime availability and session events; it exposes no pairing or session mutation. The outer management dispatcher returns available:false with HTTP200 when activation is off; CLI reports that observation with exit1 and fixed recovery guidance, distinct from an available Hub with empty lists. Client-role refusal, transport errors and malformed observations remain nonzero.

The optional terminal prototype in `src/remote-control/host.ts` invokes only a caller-supplied factory after authenticated traffic. `src/remote-control/relay.ts` routes opaque prototype envelopes after caller authorization. Neither is a production terminal service.
Expand Down
Loading
Loading