Skip to content
Merged
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
14 changes: 0 additions & 14 deletions apps/desktop/src/app/DesktopApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,20 +328,6 @@ const bootstrap = Effect.gen(function* () {
baseUrl: remoteHttpBaseUrl.href,
});
yield* desktopWindow.handleBackendReady(remoteHttpBaseUrl);

// Keep the native Windows backend available as a secondary environment
// while the remote server remains the renderer's primary backend.
const localBackend = yield* pool.get(DesktopBackendPool.WINDOWS_SECONDARY_INSTANCE_ID);
if (Option.isSome(localBackend)) {
const backendPortSelection = yield* resolveDesktopBackendPort(
environment.configuredBackendPort,
);
yield* serverExposure.configureFromSettings({ port: backendPortSelection.port });
yield* localBackend.value.start;
yield* logBootstrapInfo("bootstrap Windows secondary start requested", {
port: backendPortSelection.port,
});
}
}
return;
}
Expand Down
7 changes: 2 additions & 5 deletions apps/desktop/src/backend/DesktopBackendPool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ describe("DesktopBackendPool", () => {
),
);

it.effect("keeps Windows registered as a secondary for a remote primary", () =>
it.effect("does not register a local backend for a remote primary", () =>
Effect.scoped(
Effect.gen(function* () {
const labelRef = yield* Ref.make("Windows");
Expand All @@ -166,10 +166,7 @@ describe("DesktopBackendPool", () => {
);

assert.isTrue(Option.isNone(yield* pool.primary));
assert.deepEqual(
(yield* pool.list).map((instance) => instance.id),
[DesktopBackendPool.WINDOWS_SECONDARY_INSTANCE_ID],
);
assert.deepEqual(yield* pool.list, []);
}),
),
);
Expand Down
39 changes: 7 additions & 32 deletions apps/desktop/src/backend/DesktopBackendPool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ const { logWarning: logBackendPoolWarning } =
export type BackendInstanceId = DesktopBackendManager.BackendInstanceId;
export const BackendInstanceId = DesktopBackendManager.BackendInstanceId;
export const PRIMARY_INSTANCE_ID = DesktopBackendManager.PRIMARY_INSTANCE_ID;
export const WINDOWS_SECONDARY_INSTANCE_ID = BackendInstanceId("windows:local");
export type DesktopBackendInstance = DesktopBackendManager.DesktopBackendInstance;
export type BackendInstanceSpec = DesktopBackendManager.BackendInstanceSpec;

Expand Down Expand Up @@ -307,43 +306,19 @@ export const layer = Layer.effect(
}),
);

// A normal desktop using a remote primary still keeps its native backend
// available as a secondary execution environment. The remote-only build is
// intentionally excluded: that distribution must never launch local work.
const windowsSecondary =
!isRemoteOnlyDesktopBuild && startupPlan.remoteOnly
? Option.some(
yield* DesktopBackendManager.makeBackendInstance({
id: WINDOWS_SECONDARY_INSTANCE_ID,
label: Effect.succeed("Windows"),
configResolve: configuration.resolvePrimary,
}),
)
: Option.none<DesktopBackendInstance>();

const instancesRef = yield* SynchronizedRef.make<
ReadonlyMap<BackendInstanceId, RegisteredInstance>
>(
new Map([
...Option.match(primary, {
onNone: () => [],
onSome: (instance) => [
Option.match(primary, {
onNone: () => new Map(),
onSome: (instance) =>
new Map([
[
DesktopBackendManager.PRIMARY_INSTANCE_ID,
{ _tag: "Active" as const, instance, scope: Option.none() },
] as const,
],
}),
...Option.match(windowsSecondary, {
onNone: () => [],
onSome: (instance) => [
[
WINDOWS_SECONDARY_INSTANCE_ID,
{ _tag: "Active" as const, instance, scope: Option.none() },
] as const,
],
}),
]),
],
]),
}),
);

const register: DesktopBackendPool["Service"]["register"] = (spec) =>
Expand Down
54 changes: 0 additions & 54 deletions apps/desktop/src/ipc/methods/window.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,27 +50,6 @@ const defaultWslInstance: DesktopBackendManager.DesktopBackendInstance = {
waitForReady: () => Effect.succeed(true),
};

const { runningDistro: _runningDistro, ...readyConfigWithoutDistro } = readyWslConfig;
const windowsSecondaryInstance: DesktopBackendManager.DesktopBackendInstance = {
...defaultWslInstance,
id: DesktopBackendPool.WINDOWS_SECONDARY_INSTANCE_ID,
label: Effect.succeed("Windows"),
currentConfig: Effect.succeed(
Option.some({
...readyConfigWithoutDistro,
executablePath: "electron.exe",
args: ["server.mjs"],
entryPath: "server.mjs",
bootstrap: {
...readyWslConfig.bootstrap,
port: 3773,
host: "127.0.0.1",
},
httpBaseUrl: new URL("http://127.0.0.1:3773"),
}),
),
};

const desktopSettingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
get: Effect.succeed(DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS),
} as unknown as DesktopAppSettings.DesktopAppSettings["Service"]);
Expand Down Expand Up @@ -100,39 +79,6 @@ describe("getLocalEnvironmentBootstraps", () => {
}).pipe(Effect.provide(Layer.merge(DesktopBackendPool.layerTest([]), remoteSettingsLayer)));
});

it.effect("publishes Windows as a secondary while the remote server remains primary", () => {
const remoteSettingsLayer = Layer.succeed(DesktopAppSettings.DesktopAppSettings, {
get: Effect.succeed({
...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
primaryBackendMode: "remote",
remoteBackendUrl: "https://remote.example.test/",
}),
} as unknown as DesktopAppSettings.DesktopAppSettings["Service"]);
return Effect.gen(function* () {
assert.deepEqual(yield* getLocalEnvironmentBootstraps.handler(), [
{
id: "primary",
label: "remote.example.test",
runningDistro: null,
httpBaseUrl: "https://remote.example.test/",
wsBaseUrl: "wss://remote.example.test/",
},
{
id: "windows:local",
label: "Windows",
runningDistro: null,
httpBaseUrl: "http://127.0.0.1:3773/",
wsBaseUrl: "ws://127.0.0.1:3773/",
bootstrapToken: "bootstrap-token",
},
]);
}).pipe(
Effect.provide(
Layer.merge(DesktopBackendPool.layerTest([windowsSecondaryInstance]), remoteSettingsLayer),
),
);
});

it.effect("publishes the concrete running distro without replacing the stable instance id", () =>
Effect.gen(function* () {
const result = yield* getLocalEnvironmentBootstraps.handler();
Expand Down
6 changes: 1 addition & 5 deletions apps/desktop/src/ipc/methods/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,7 @@ export const pickFolder = DesktopIpc.makeIpcMethod({
targetId !== PRIMARY_LOCAL_ENVIRONMENT_ID &&
targetId.startsWith(DesktopWslBackend.WSL_INSTANCE_ID_PREFIX);
const settings = yield* appSettings.get;
if (
settings.primaryBackendMode === "remote" &&
!isLocalExecutionOverride() &&
targetId !== DesktopBackendPool.WINDOWS_SECONDARY_INSTANCE_ID
) {
if (settings.primaryBackendMode === "remote" && !isLocalExecutionOverride()) {
// A native Windows folder path is meaningless to the remote Linux
// environment. Remote project selection stays server-side.
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,30 @@ describe("runtimeEventToActivities approval details", () => {
expect(activity?.kind).toBe("approval.requested");
expect((activity?.payload as Record<string, unknown> | undefined)?.detail).toBe(detail);
});

it("gives a sandbox permission request an answerable request kind", () => {
const event = {
type: "request.opened",
eventId: EventId.make("evt-permissions-opened"),
provider: ProviderDriverKind.make("codex"),
createdAt: "2026-07-18T00:00:00.000Z",
threadId: ThreadId.make("thread-1"),
requestId: RuntimeRequestId.make("approval-perm-1"),
payload: {
requestType: "permissions_approval",
detail: "read: /workspace/src",
},
} satisfies ProviderRuntimeEvent;

const [activity] = runtimeEventToActivities(event);
const payload = activity?.payload as Record<string, unknown> | undefined;

expect(activity?.kind).toBe("approval.requested");
// Without a requestKind the web and mobile approval folds never render a
// prompt, while the server still opens a pending row — the thread parks
// with no way to answer it.
expect(payload?.requestKind).toBe("file-change");
expect(activity?.summary).toBe("Sandbox permission approval requested");
expect(payload?.detail).toBe("read: /workspace/src");
});
});
20 changes: 13 additions & 7 deletions apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,6 +718,10 @@ function requestKindFromCanonicalRequestType(
return "file-read";
case "file_change_approval":
case "apply_patch_approval":
// A Codex sandbox escalation can grant writes, so it rides the file-change
// kind. Must match CODEX_PERMISSION_REQUEST_KIND, which the session runtime
// stamps on the request itself.
case "permissions_approval":
return "file-change";
default:
return undefined;
Expand Down Expand Up @@ -793,13 +797,15 @@ export function runtimeEventToActivities(
tone: "approval",
kind: "approval.requested",
summary:
requestKind === "command"
? "Command approval requested"
: requestKind === "file-read"
? "File-read approval requested"
: requestKind === "file-change"
? "File-change approval requested"
: "Approval requested",
event.payload.requestType === "permissions_approval"
? "Sandbox permission approval requested"
: requestKind === "command"
? "Command approval requested"
: requestKind === "file-read"
? "File-read approval requested"
: requestKind === "file-change"
? "File-change approval requested"
: "Approval requested",
payload: {
requestId: toApprovalRequestId(event.requestId),
...(requestKind ? { requestKind } : {}),
Expand Down
60 changes: 59 additions & 1 deletion apps/server/src/provider/Layers/CodexAdapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,14 +286,23 @@ const providerSessionDirectoryTestLayer = Layer.succeed(ProviderSessionDirectory
});

const validationRuntimeFactory = makeRuntimeFactory();
// Isolation requires a source auth.json before any session starts, so these
// validation cases need a real home even though none of them assert on auth.
const validationSourceHomePath = NodeFS.mkdtempSync(
NodePath.join(NodeOS.tmpdir(), "cc-codex-validation-home-"),
);
NodeFS.writeFileSync(
NodePath.join(validationSourceHomePath, "auth.json"),
'{"token":"test-only"}\n',
);
const validationLayer = it.layer(
Layer.effect(
CodexAdapter,
Effect.gen(function* () {
const codexConfig = decodeCodexSettings({});
return yield* makeCodexAdapter(codexConfig, {
makeRuntime: validationRuntimeFactory.factory,
commandCenterSourceHomePath: NodePath.join(process.cwd(), ".missing-command-center-auth"),
commandCenterSourceHomePath: validationSourceHomePath,
commandCenterRuntimeExecutablePath: process.execPath,
});
}),
Expand Down Expand Up @@ -444,6 +453,7 @@ it.effect("fails closed when elevated Windows Command Center isolation cannot be
const runtimePath = NodePath.join(tempDir, "codex.exe");
const sourceHomePath = NodePath.join(tempDir, "codex-home");
NodeFS.mkdirSync(sourceHomePath, { recursive: true });
NodeFS.writeFileSync(NodePath.join(sourceHomePath, "auth.json"), '{"token":"test-only"}\n');
NodeFS.writeFileSync(runtimePath, Uint8Array.from([0x4d, 0x5a, 0x00, 0x00]));

const runtimeFactory = makeRuntimeFactory({
Expand Down Expand Up @@ -2161,6 +2171,54 @@ lifecycleLayer("CodexAdapterLive lifecycle", (it) => {
}),
);

it.effect("surfaces a sandbox permission request with the paths it asks for", () =>
Effect.gen(function* () {
const { adapter, runtime } = yield* startLifecycleRuntime();
const firstEventFiber = yield* Stream.runHead(adapter.streamEvents).pipe(Effect.forkChild);

const event: ProviderEvent = {
id: asEventId("evt-permissions-request"),
kind: "request",
provider: ProviderDriverKind.make("codex"),
threadId: asThreadId("thread-1"),
createdAt: "2026-01-01T00:00:00.000Z",
method: "item/permissions/requestApproval",
requestKind: "file-change",
requestId: ApprovalRequestId.make("req-perm-1"),
payload: {
cwd: "/workspace",
itemId: "item-1",
permissions: {
fileSystem: {
entries: [{ access: "write", path: { type: "path", path: "/workspace/dist" } }],
},
},
reason: "write build output",
startedAtMs: 0,
threadId: "thread-1",
turnId: "turn-1",
},
};

yield* runtime.emit(event);
const firstEvent = yield* Fiber.join(firstEventFiber);

NodeAssert.equal(firstEvent._tag, "Some");
if (firstEvent._tag !== "Some") {
return;
}
NodeAssert.equal(firstEvent.value.type, "request.opened");
if (firstEvent.value.type !== "request.opened") {
return;
}
// Before this method was mapped it fell through to "unknown", which the
// web and mobile approval folds silently drop.
NodeAssert.equal(firstEvent.value.payload.requestType, "permissions_approval");
NodeAssert.match(firstEvent.value.payload.detail ?? "", /write: \/workspace\/dist/u);
NodeAssert.match(firstEvent.value.payload.detail ?? "", /write build output/u);
}),
);

it.effect("preserves file-read request type when mapping serverRequest/resolved", () =>
Effect.gen(function* () {
const { adapter, runtime } = yield* startLifecycleRuntime();
Expand Down
17 changes: 16 additions & 1 deletion apps/server/src/provider/Layers/CodexAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ import {
resolveCommandCenterCodexRuntimeExecutable,
resolveCommandCenterManagedGitMetadata,
} from "../security/CommandCenterProviderIsolation.ts";
import { describeCodexPermissionRequest } from "../security/CodexPermissionEscalation.ts";
import { resolveCodexLaunchArgs } from "./codexLaunchArgs.ts";
const isCodexAppServerProcessExitedError = Schema.is(CodexErrors.CodexAppServerProcessExitedError);
const isCodexAppServerTransportError = Schema.is(CodexErrors.CodexAppServerTransportError);
Expand Down Expand Up @@ -453,6 +454,8 @@ function toRequestTypeFromMethod(method: string): CanonicalRequestType {
return "exec_command_approval";
case "item/tool/requestUserInput":
return "tool_user_input";
case "item/permissions/requestApproval":
return "permissions_approval";
case "item/tool/call":
return "dynamic_tool_call";
case "account/chatgptAuthTokens/refresh":
Expand Down Expand Up @@ -1120,6 +1123,15 @@ function mapToRuntimeEvents(
);
return payload?.reason ?? payload?.command.join(" ");
}
case "item/permissions/requestApproval": {
const payload = readPayload(
EffectCodexSchema.ServerRequest__PermissionsRequestApprovalParams,
event.payload,
);
// The persisted approval activity keeps only `detail`, so the
// requested paths are folded in here or the prompt is unreviewable.
return payload ? describeCodexPermissionRequest(payload) : undefined;
}
case "item/tool/call": {
const payload = readPayload(
EffectCodexSchema.ServerRequest__DynamicToolCallParams,
Expand Down Expand Up @@ -2009,7 +2021,7 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
? yield* (
options?.commandCenterRuntimeExecutablePath
? Effect.succeed(options.commandCenterRuntimeExecutablePath)
: resolveCommandPath(codexConfig.binaryPath, {
: resolveCommandPath("codex", {
env: sourceEnvironment,
extendEnv: false,
}).pipe(
Expand Down Expand Up @@ -2160,6 +2172,9 @@ export const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (
...(commandCenterIsolation
? { permissionProfile: commandCenterIsolation.permissionProfile }
: {}),
...(codexConfig.autoApproveReadOnlyPermissions
? { autoApproveReadOnlyPermissions: true }
: {}),
...(commandCenterIsolation?.windowsSandboxMode
? {
commandCenterPlatform: hostPlatform,
Expand Down
Loading
Loading