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
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ vi.mock("../../../state/use-selected-thread-git-actions", () => ({
useSelectedThreadGitActions: () => ({ canChangeThreadBranch: true }),
}));
vi.mock("../../../state/query", () => ({ useEnvironmentQuery: () => ({ data: null }) }));
vi.mock("../../../state/vcs", () => ({ vcsEnvironment: { status: () => null } }));
vi.mock("../../../state/vcs", () => ({ vcsEnvironment: { branch: () => null } }));

import { GitBranchesSheet } from "./GitBranchesSheet";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ vi.mock("../../../state/use-selected-thread-git-actions", () => ({
},
}),
}));
vi.mock("../../../state/vcs", () => ({ vcsEnvironment: { status: () => null } }));
vi.mock("../../../state/vcs", () => ({ vcsEnvironment: { branch: () => null } }));
vi.mock("./gitSheetComponents", () => ({
SheetActionButton: (props: ActionProps) => {
state.actions.set(props.label, props);
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/features/threads/git/GitBranchesSheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export function GitBranchesSheet(_props: GitBranchesSheetProps) {

const gitStatus = useEnvironmentQuery(
selectedThread !== null && selectedThreadCwd !== null
? vcsEnvironment.status({
? vcsEnvironment.branch({
environmentId: selectedThread.environmentId,
input: { cwd: selectedThreadCwd },
})
Expand Down
61 changes: 61 additions & 0 deletions apps/server/src/auth/RpcAuthorization.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ describe("RPC authorization scopes", () => {
}
});

it("lets thread readers observe and refresh checkout branches", () => {
for (const method of [WS_METHODS.subscribeVcsBranch, WS_METHODS.vcsRefreshBranch]) {
expect(requiredScopeForRpcMethod(method)).toBe(AuthOrchestrationReadScope);
}
});

it("rejects unknown RPC method names", () => {
for (const method of ["server.notRegistered", "toString", "constructor"]) {
expect(() => requiredScopeForRpcMethod(method)).toThrow(
Expand Down Expand Up @@ -133,6 +139,61 @@ describe("RPC scope middleware", () => {
);
});

it.effect.each([WS_METHODS.subscribeVcsBranch, WS_METHODS.subscribeVcsStatus] as const)(
"enforces read permission on stream %s",
(method) =>
Effect.gen(function* () {
const group = WsRpcGroup.omit(
...[...WsRpcGroup.requests.keys()].filter(
(
tag,
): tag is Exclude<
keyof typeof RPC_REQUIRED_SCOPES,
typeof WS_METHODS.subscribeVcsBranch | typeof WS_METHODS.subscribeVcsStatus
> => tag !== WS_METHODS.subscribeVcsBranch && tag !== WS_METHODS.subscribeVcsStatus,
),
);
let handled = 0;
const handlers = Layer.mergeAll(
group.toLayerHandler(WS_METHODS.subscribeVcsBranch, () =>
Stream.fromEffect(
Effect.sync(() => {
handled++;
return { refName: "main" };
}),
),
),
group.toLayerHandler(WS_METHODS.subscribeVcsStatus, () =>
Stream.fromEffect(
Effect.sync(() => {
handled++;
}),
).pipe(Stream.drain),
),
);
const denied = yield* RpcTest.makeClient(group).pipe(
Effect.provide(Layer.merge(handlers, RpcAuthorization.layer([]))),
);
expect(
yield* Stream.runDrain(
method === WS_METHODS.subscribeVcsBranch
? denied[method]({ cwd: "/repo" }).pipe(Stream.drain)
: denied[method]({ cwd: "/repo" }).pipe(Stream.drain),
).pipe(Effect.flip),
).toMatchObject({ requiredPermission: AuthOrchestrationReadScope });
expect(handled).toBe(0);
const granted = yield* RpcTest.makeClient(group).pipe(
Effect.provide(Layer.merge(handlers, RpcAuthorization.layer([AuthOrchestrationReadScope]))),
);
yield* Stream.runDrain(
method === WS_METHODS.subscribeVcsBranch
? granted[method]({ cwd: "/repo" }).pipe(Stream.drain)
: granted[method]({ cwd: "/repo" }).pipe(Stream.drain),
);
expect(handled).toBe(1);
}).pipe(Effect.scoped),
);

describe("settings mutation authorization", () => {
const group = WsRpcGroup.omit(
...[...WsRpcGroup.requests.keys()].filter(
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.mcpAppsReadResource]: AuthOrchestrationReadScope,
[WS_METHODS.mcpAppsUpdateModelContext]: AuthOrchestrationOperateScope,
[WS_METHODS.subscribeVcsStatus]: AuthOrchestrationReadScope,
[WS_METHODS.subscribeVcsBranch]: AuthOrchestrationReadScope,
[WS_METHODS.vcsRefreshBranch]: AuthOrchestrationReadScope,
[WS_METHODS.subscribeWorktreeSetup]: AuthOrchestrationReadScope,
[WS_METHODS.worktreeSetupCancel]: AuthOrchestrationOperateScope,
[WS_METHODS.subscribeResourceTelemetry]: AuthDiagnosticsReadScope,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,7 @@ export const make = Effect.gen(function* () {
pullRequests: true,
pullRequestChecks: true,
sideChats: true,
vcsBranchStatus: true,
inlineMessageContext: true,
requiredWorktreeBootstrap: true,
threadSettlement: true,
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/observability/RpcInstrumentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@ const RPC_AGGREGATES = {
[WS_METHODS.mcpAppsUpdateModelContext]: "provider",
[WS_METHODS.mcpAppsReadResource]: "provider",
[WS_METHODS.subscribeVcsStatus]: "vcs",
[WS_METHODS.subscribeVcsBranch]: "vcs",
[WS_METHODS.vcsRefreshBranch]: "vcs",
[WS_METHODS.subscribeWorktreeSetup]: "vcs",
[WS_METHODS.worktreeSetupCancel]: "vcs",
[WS_METHODS.subscribeResourceTelemetry]: "server",
Expand Down
Loading
Loading