Skip to content

Commit 464e2b3

Browse files
realhasanshoaibjuliusmarmingeclaude
committed
fix(web): gate host file actions on destination permission
Opening or revealing a file runs on the destination's host, so the file context menu now hides those actions when that environment's session lacks the operate grant, and the command is guarded client-side like the server does. Co-authored-by: Julius Marminge <[email protected]> Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent 1c9dfca commit 464e2b3

3 files changed

Lines changed: 38 additions & 3 deletions

File tree

‎apps/web/src/fileContextMenu.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,15 @@ function buildFileContextMenuItems(input: {
8989
export function useFileContextMenu(environmentId: EnvironmentId | null) {
9090
const openInEditor = useAtomCommand(shellEnvironment.openInEditor, { reportFailure: false });
9191
const serverConfig = useAtomValue(serverEnvironment.configValueAtom(environmentId));
92+
const canOpenInEditor = useAtomValue(shellEnvironment.openInEditor.permissionAtom(environmentId));
9293

93-
const availableEditors = serverConfig?.availableEditors ?? [];
94+
const availableEditors = canOpenInEditor ? (serverConfig?.availableEditors ?? []) : [];
9495
const capabilities: FileContextMenuCapabilities = {
9596
// WSL can reveal in Windows Explorer despite reporting Linux; use server wording.
9697
revealLabel:
9798
environmentId !== null &&
9899
serverConfig?.shellRevealInFileManager === true &&
99-
serverConfig.availableEditors.includes("file-manager")
100+
availableEditors.includes("file-manager")
100101
? serverConfig.shellRevealInFileManagerKind === undefined
101102
? revealInFileExplorerLabelForOs(serverConfig.environment.platform.os)
102103
: revealInFileExplorerLabelForKind(serverConfig.shellRevealInFileManagerKind)
@@ -117,7 +118,8 @@ export function useFileContextMenu(environmentId: EnvironmentId | null) {
117118
action === "open" || reveal
118119
? ("file-manager" as const)
119120
: (action.slice("editor:".length) as EditorId);
120-
if (action !== "open" && !reveal && !capabilities.editorIds.includes(editor)) return;
121+
if (!capabilities.editorIds.includes(editor)) return;
122+
if (reveal && capabilities.revealLabel === undefined) return;
121123

122124
const result = await openInEditor({
123125
environmentId,

‎packages/client-runtime/src/state/commandPermissions.test.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
AuthAccessReadScope,
1111
AuthAccessWriteScope,
1212
AuthEnvironmentMaintainScope,
13+
AuthFilesystemReadScope,
1314
AuthOrchestrationOperateScope,
1415
AuthOrchestrationReadScope,
1516
AuthPreviewOperateScope,
@@ -33,6 +34,7 @@ import { Atom, AtomRegistry, AsyncResult } from "effect/reactivity";
3334
import { EnvironmentRegistry } from "../connection/registry.ts";
3435
import { createCommandPermissions } from "./commandPermissions.ts";
3536
import { createEnvironmentRpcCommand } from "./runtime.ts";
37+
import { createShellEnvironmentAtoms } from "./shellCommands.ts";
3638
import { AVAILABLE_CONNECTION_STATE, BearerConnectionTarget } from "../connection/model.ts";
3739
import { createPeerLinkEnvironmentAtoms } from "./peerLinks.ts";
3840

@@ -592,3 +594,33 @@ it.effect("rejects protected unary and streamed RPCs outside a guarded command",
592594
expect(writes).toBe(0);
593595
}),
594596
);
597+
598+
it.effect("requires the destination operate grant to open or reveal a file on its host", () =>
599+
Effect.scoped(
600+
Effect.gen(function* () {
601+
const registry = yield* setup;
602+
const { openInEditor } = createShellEnvironmentAtoms(runtime);
603+
const authorization = createCommandPermissions(runtime, WS_METHODS.shellOpenInEditor);
604+
registry.set(sessions(other), AsyncResult.success(grant(true)));
605+
registry.set(
606+
sessions(env),
607+
AsyncResult.success({
608+
...grant(false),
609+
scopes: [AuthFilesystemReadScope],
610+
permissions: [AuthFilesystemReadScope],
611+
}),
612+
);
613+
expect(registry.get(openInEditor.permissionAtom(other))).toBe(true);
614+
expect(registry.get(openInEditor.permissionAtom(env))).toBe(false);
615+
const denied = yield* authorization.authorize(registry, env).pipe(Effect.flip);
616+
expect(denied).toMatchObject({
617+
_tag: "EnvironmentAuthorizationError",
618+
requiredPermission: AuthOrchestrationOperateScope,
619+
});
620+
621+
registry.set(sessions(env), AsyncResult.success(grant(true)));
622+
expect(registry.get(openInEditor.permissionAtom(env))).toBe(true);
623+
yield* authorization.authorize(registry, env);
624+
}),
625+
),
626+
);

‎packages/contracts/src/clientRpcPermissions.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export const CLIENT_GUARDED_RPC_SCOPES = {
6363

6464
// Starts an agent session on this environment that can read its logs and database.
6565
[WS_METHODS.serverPrepareTriage]: AuthOrchestrationOperateScope,
66+
[WS_METHODS.shellOpenInEditor]: AuthOrchestrationOperateScope,
6667
[WS_METHODS.scheduledTasksUpsert]: AuthOrchestrationOperateScope,
6768
[WS_METHODS.scheduledTasksSetEnabled]: AuthOrchestrationOperateScope,
6869
[WS_METHODS.scheduledTasksDelete]: AuthOrchestrationOperateScope,

0 commit comments

Comments
 (0)