From 2602632166c0b0b455580316869bb5bc79e6aa99 Mon Sep 17 00:00:00 2001 From: only21mil Date: Sat, 31 Oct 2026 19:31:24 +0000 Subject: [PATCH 1/2] fix(mobile): Save plan to workspace is off for connections without file write access projects.writeFile was missing from CLIENT_GUARDED_RPC_SCOPES, so its permissionAtom was always true and mobile's Save to workspace button stayed enabled for sessions without filesystem:write, failing only on the server. Move the entry into the client map that the server already spreads. Co-Authored-By: Claude Opus 5.5 --- apps/server/src/auth/RpcAuthorization.ts | 2 -- .../src/state/commandPermissions.test.ts | 22 +++++++++++++++++++ .../src/state/projectCommands.test.ts | 1 + .../contracts/src/clientRpcPermissions.ts | 2 ++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/apps/server/src/auth/RpcAuthorization.ts b/apps/server/src/auth/RpcAuthorization.ts index 6049121b41a5..a7043010f9b6 100644 --- a/apps/server/src/auth/RpcAuthorization.ts +++ b/apps/server/src/auth/RpcAuthorization.ts @@ -14,7 +14,6 @@ import { AuthProvidersManageScope, AuthEnvironmentMaintainScope, AuthFilesystemReadScope, - AuthFilesystemWriteScope, AuthDiagnosticsReadScope, AuthOrchestrationOperateScope, AuthOrchestrationReadScope, @@ -148,7 +147,6 @@ export const RPC_REQUIRED_SCOPES = { [WS_METHODS.projectsReadFile]: AuthFilesystemReadScope, [WS_METHODS.projectsSearchContents]: AuthFilesystemReadScope, [WS_METHODS.projectsSearchEntries]: AuthFilesystemReadScope, - [WS_METHODS.projectsWriteFile]: AuthFilesystemWriteScope, [WS_METHODS.projectsEnsureScratch]: AuthOrchestrationOperateScope, [WS_METHODS.projectsCreateNew]: AuthOrchestrationOperateScope, [WS_METHODS.shellOpenInEditor]: AuthOrchestrationOperateScope, diff --git a/packages/client-runtime/src/state/commandPermissions.test.ts b/packages/client-runtime/src/state/commandPermissions.test.ts index adfad46a41b8..7398f2ae063a 100644 --- a/packages/client-runtime/src/state/commandPermissions.test.ts +++ b/packages/client-runtime/src/state/commandPermissions.test.ts @@ -9,6 +9,7 @@ import { vi } from "vite-plus/test"; import { AuthAccessReadScope, AuthAccessWriteScope, + AuthFilesystemWriteScope, AuthOrchestrationOperateScope, AuthOrchestrationReadScope, AuthPreviewOperateScope, @@ -96,6 +97,27 @@ describe("command permissions", () => { ), ); + it.effect("requires the destination filesystem write grant to write a file", () => + Effect.scoped( + Effect.gen(function* () { + const registry = yield* setup; + const write = createCommandPermissions(runtime, WS_METHODS.projectsWriteFile); + registry.set(sessions(env), AsyncResult.success(grant(true))); + expect(registry.get(write.permissionAtom(env))).toBe(false); + registry.set( + sessions(env), + AsyncResult.success({ + ...grant(false), + scopes: [AuthFilesystemWriteScope], + permissions: [AuthFilesystemWriteScope], + }), + ); + expect(registry.get(write.permissionAtom(env))).toBe(true); + yield* write.authorize(registry, env); + }), + ), + ); + it.effect("requires the destination settings grant to run storage cleanup", () => Effect.scoped( Effect.gen(function* () { diff --git a/packages/client-runtime/src/state/projectCommands.test.ts b/packages/client-runtime/src/state/projectCommands.test.ts index 727dd87ff686..4e6a44138aba 100644 --- a/packages/client-runtime/src/state/projectCommands.test.ts +++ b/packages/client-runtime/src/state/projectCommands.test.ts @@ -124,6 +124,7 @@ const makeHarness = Effect.fn("TestProjectCommands.makeHarness")(function* () { }); const registry = AtomRegistry.make(); yield* Effect.addFinalizer(() => Effect.sync(() => registry.dispose())); + registry.mount(sessions(ENVIRONMENT_ID)); registry.set( sessions(ENVIRONMENT_ID), AsyncResult.success({ diff --git a/packages/contracts/src/clientRpcPermissions.ts b/packages/contracts/src/clientRpcPermissions.ts index d3139f2eeff2..ff4d99d42ea4 100644 --- a/packages/contracts/src/clientRpcPermissions.ts +++ b/packages/contracts/src/clientRpcPermissions.ts @@ -2,6 +2,7 @@ import * as Schema from "effect/Schema"; import { GitPreparePullRequestThreadInput, GitRunStackedActionInput } from "./git.ts"; import { AuthAccessWriteScope, + AuthFilesystemWriteScope, AuthOrchestrationOperateScope, AuthOrchestrationReadScope, AuthPreviewOperateScope, @@ -33,6 +34,7 @@ export const CLIENT_GUARDED_RPC_SCOPES = { [WS_METHODS.pullRequestsSetLabels]: AuthSourceControlWriteScope, // Writing a guide runs a model on this environment's providers, which reading one does not. [WS_METHODS.pullRequestsGenerateReviewGuide]: AuthOrchestrationOperateScope, + [WS_METHODS.projectsWriteFile]: AuthFilesystemWriteScope, [WS_METHODS.projectsCreateFromThread]: AuthOrchestrationOperateScope, [WS_METHODS.sourceControlCloneRepository]: AuthSourceControlWriteScope, [WS_METHODS.sourceControlPublishRepository]: AuthSourceControlWriteScope, From f8c62dc9385ecd1639f85d5b17f4d5eaf44a982a Mon Sep 17 00:00:00 2001 From: Victor Vogel Date: Tue, 10 Nov 2026 13:29:09 +0000 Subject: [PATCH 2/2] test(client-runtime): check file writes as a row in the scope permission table Main now has a table of commands that each need one destination scope. The projects.writeFile case moves into it, so it also checks that authorize fails without the grant and that another environment's grant does not count. Co-Authored-By: Claude Opus 5.5 --- .../src/state/commandPermissions.test.ts | 22 +------------------ 1 file changed, 1 insertion(+), 21 deletions(-) diff --git a/packages/client-runtime/src/state/commandPermissions.test.ts b/packages/client-runtime/src/state/commandPermissions.test.ts index 5494075e26ca..67488a70dad9 100644 --- a/packages/client-runtime/src/state/commandPermissions.test.ts +++ b/packages/client-runtime/src/state/commandPermissions.test.ts @@ -133,27 +133,6 @@ describe("command permissions", () => { ), ); - it.effect("requires the destination filesystem write grant to write a file", () => - Effect.scoped( - Effect.gen(function* () { - const registry = yield* setup; - const write = createCommandPermissions(runtime, WS_METHODS.projectsWriteFile); - registry.set(sessions(env), AsyncResult.success(grant(true))); - expect(registry.get(write.permissionAtom(env))).toBe(false); - registry.set( - sessions(env), - AsyncResult.success({ - ...grant(false), - scopes: [AuthFilesystemWriteScope], - permissions: [AuthFilesystemWriteScope], - }), - ); - expect(registry.get(write.permissionAtom(env))).toBe(true); - yield* write.authorize(registry, env); - }), - ), - ); - it.effect("requires the destination provider grant to remove an incomplete connection", () => Effect.scoped( Effect.gen(function* () { @@ -183,6 +162,7 @@ describe("command permissions", () => { it.effect.each([ { method: WS_METHODS.serverRunStorageCleanup, scope: AuthSettingsWriteScope }, { method: WS_METHODS.serverReclaimDatabaseSpace, scope: AuthEnvironmentMaintainScope }, + { method: WS_METHODS.projectsWriteFile, scope: AuthFilesystemWriteScope }, ])("requires the destination $scope grant for $method", ({ method, scope }) => Effect.scoped( Effect.gen(function* () {