Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
9 changes: 6 additions & 3 deletions apps/ade-cli/package-lock.json

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

27 changes: 27 additions & 0 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3012,6 +3012,33 @@ describe("adeRpcServer", () => {
});
});

it("uses initialized chat session identity when the server process has no ADE_CHAT_SESSION_ID", async () => {
await withEnv({ ADE_CHAT_SESSION_ID: undefined, ADE_DEFAULT_ROLE: "agent" }, async () => {
const fixture = createRuntime();
fixture.runtime.agentChatService.requestChatInput = vi.fn(async () => ({
decision: "decline",
answers: {},
responseText: null,
}));
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });

await initialize(handler, {
callerId: "chat-session-identity",
role: "agent",
chatSessionId: "chat-session-identity",
});
const response = await callTool(handler, "ask_user", {
title: "Pick a flow",
body: "Which part should we test first?",
});

expect(response?.isError).toBeUndefined();
expect(fixture.runtime.agentChatService.requestChatInput).toHaveBeenCalledWith(expect.objectContaining({
chatSessionId: "chat-session-identity",
}));
});
});

it("returns explicit timed_out semantics for standalone ask_user when the user does not answer in time", async () => {
await withEnv({ ADE_CHAT_SESSION_ID: "chat-session-env" }, async () => {
const fixture = createRuntime();
Expand Down
26 changes: 15 additions & 11 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2889,10 +2889,10 @@ function resolveCallerContext(session?: SessionState): CallerContext {
return {
callerId: asOptionalTrimmedString(session.identity.callerId),
role: session.identity.role ?? envContext.role,
chatSessionId: envContext.chatSessionId,
chatSessionId: session.identity.chatSessionId ?? envContext.chatSessionId,
standaloneChatSession: session.identity.standaloneChatSession,
missionId: session.identity.missionId ?? envContext.missionId,
runId: envContext.runId,
runId: session.identity.runId ?? envContext.runId,
stepId: session.identity.stepId ?? envContext.stepId,
attemptId: session.identity.attemptId ?? envContext.attemptId,
ownerId: session.identity.ownerId ?? envContext.ownerId,
Expand Down Expand Up @@ -2995,7 +2995,11 @@ function parseInitializeIdentity(runtime: AdeRuntime, params: unknown): SessionI
? identityRole
: null;
const validRole: SessionIdentity["role"] = envContext.role ?? "external";
const resolvedRunId = envContext.runId;
const requestedChatSessionId = asOptionalTrimmedString(identity.chatSessionId);
const resolvedChatSessionId = envContext.chatSessionId ?? requestedChatSessionId;
const resolvedRunId = envContext.runId ?? asOptionalTrimmedString(identity.runId);
const resolvedStepId = envContext.stepId ?? asOptionalTrimmedString(identity.stepId);
const resolvedAttemptId = envContext.attemptId ?? asOptionalTrimmedString(identity.attemptId);
const requestedMissionId = asOptionalTrimmedString(identity.missionId);
const resolvedMissionId =
envContext.missionId
Expand All @@ -3007,21 +3011,21 @@ function parseInitializeIdentity(runtime: AdeRuntime, params: unknown): SessionI
);
}

const standaloneChatSession = Boolean(envContext.chatSessionId)
const standaloneChatSession = Boolean(resolvedChatSessionId)
&& !envContext.missionId
&& !envContext.runId
&& !envContext.stepId
&& !envContext.attemptId;
&& !resolvedRunId
&& !resolvedStepId
&& !resolvedAttemptId;

return {
callerId: asOptionalTrimmedString(identity.callerId) ?? envContext.chatSessionId ?? envContext.attemptId ?? "unknown",
callerId: asOptionalTrimmedString(identity.callerId) ?? resolvedChatSessionId ?? envContext.attemptId ?? "unknown",
role: validRole,
chatSessionId: envContext.chatSessionId,
chatSessionId: resolvedChatSessionId,
standaloneChatSession,
missionId: resolvedMissionId ?? requestedMissionId ?? null,
runId: resolvedRunId,
stepId: asOptionalTrimmedString(identity.stepId) ?? envContext.stepId,
attemptId: asOptionalTrimmedString(identity.attemptId) ?? envContext.attemptId,
stepId: resolvedStepId,
attemptId: resolvedAttemptId,
ownerId: asOptionalTrimmedString(identity.ownerId) ?? envContext.ownerId,
};
}
Expand Down
14 changes: 13 additions & 1 deletion apps/ade-cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3790,12 +3790,24 @@ export function shouldAttemptDesktopSocketConnection(socketPath: string): boolea
}

async function initializeConnection(connection: CliConnection, options: GlobalOptions): Promise<void> {
const envChatSessionId = asString(process.env.ADE_CHAT_SESSION_ID);
const envMissionId = asString(process.env.ADE_MISSION_ID);
const envRunId = asString(process.env.ADE_RUN_ID);
const envStepId = asString(process.env.ADE_STEP_ID);
const envAttemptId = asString(process.env.ADE_ATTEMPT_ID);
const envOwnerId = asString(process.env.ADE_OWNER_ID);
await connection.request("ade/initialize", {
protocolVersion: PROTOCOL_VERSION,
clientInfo: { name: "ade-cli", version: VERSION },
identity: {
callerId: "ade-cli",
callerId: envChatSessionId ?? envAttemptId ?? "ade-cli",
role: options.role,
...(envChatSessionId ? { chatSessionId: envChatSessionId } : {}),
...(envMissionId ? { missionId: envMissionId } : {}),
...(envRunId ? { runId: envRunId } : {}),
...(envStepId ? { stepId: envStepId } : {}),
...(envAttemptId ? { attemptId: envAttemptId } : {}),
...(envOwnerId ? { ownerId: envOwnerId } : {}),
computerUsePolicy: {
mode: "auto",
allowLocalFallback: options.role !== "external",
Expand Down
114 changes: 93 additions & 21 deletions apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import type { PortLease, ProjectInfo, SyncMobileProjectSummary, SyncProjectConne
import type { AutomationTriggerType } from "../shared/types/config";
import type { AutomationTriggerLinearIssueContext } from "../shared/types/automations";
import type { LinearIngressEventRecord } from "../shared/types/linearSync";
import type { IosSimulatorDrawerMode } from "../shared/types/iosSimulator";
import type { AppContext } from "./services/ipc/registerIpc";
import fs from "node:fs";
import net from "node:net";
Expand Down Expand Up @@ -118,6 +119,7 @@ import { createWorkerAdapterRuntimeService } from "./services/cto/workerAdapterR
import { createWorkerTaskSessionService } from "./services/cto/workerTaskSessionService";
import { createWorkerHeartbeatService } from "./services/cto/workerHeartbeatService";
import { createLinearCredentialService } from "./services/cto/linearCredentialService";
import { buildRendererCspPolicy } from "./rendererCsp";
import { createLinearClient } from "./services/cto/linearClient";
import { createLinearIssueTracker } from "./services/cto/linearIssueTracker";
import { createLinearTemplateService } from "./services/cto/linearTemplateService";
Expand Down Expand Up @@ -468,26 +470,7 @@ async function createWindow(args: {

// Set CSP dynamically so it works with both http:// (dev) and file:// (production).
const isDevMode = !!process.env.VITE_DEV_SERVER_URL;
const cspSources = isDevMode
? "'self' http://localhost:* http://127.0.0.1:*"
: "'self' file: app:";
const cspWsSources = isDevMode ? " ws://localhost:* ws://127.0.0.1:*" : "";
const cspLocalSources = " http://localhost:* http://127.0.0.1:*";
const cspImageSources = `${cspSources}${cspLocalSources} https://avatars.githubusercontent.com https://*.githubusercontent.com https://github.githubassets.com https://opengraph.githubassets.com https://github.com https://vercel.com https://*.vercel.com https://img.shields.io https://*.s3.amazonaws.com`;
const cspPolicy = [
`default-src ${cspSources}`,
`base-uri 'self'`,
`form-action 'self'`,
`object-src 'none'`,
`frame-src ${cspSources}${cspLocalSources} about:`,
`script-src ${cspSources} 'unsafe-inline'`,
`style-src ${cspSources} 'unsafe-inline'`,
`img-src ${cspImageSources} ade-artifact: data: blob:`,
`media-src ${cspSources}${cspLocalSources} ade-artifact: blob: data:`,
`font-src ${cspSources} data:`,
`connect-src ${cspSources}${cspWsSources} https:`,
`worker-src 'self' blob:`,
].join("; ");
const cspPolicy = buildRendererCspPolicy(isDevMode);

win.webContents.session.webRequest.onHeadersReceived((details, callback) => {
callback({
Expand Down Expand Up @@ -2767,6 +2750,95 @@ app.whenReady().then(async () => {
onEvent: (payload) =>
emitProjectEvent(projectRoot, IPC.iosSimulatorEvent, payload),
});
const iosSimulatorDrawerActionModes: Partial<Record<string, IosSimulatorDrawerMode>> = {
inspectPoint: "inspect",
launch: "interact",
openPreviewWorkspace: "preview",
renderPreview: "preview",
selectPoint: "inspect",
startStream: "interact",
tap: "interact",
typeText: "interact",
drag: "interact",
swipe: "interact",
};
const requestIosSimulatorDrawerOpen = (
action: keyof typeof iosSimulatorDrawerActionModes,
rawArgs: unknown,
result?: unknown,
): void => {
const mode = iosSimulatorDrawerActionModes[action];
if (!mode) return;
const argRecord = rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs)
? rawArgs as Record<string, unknown>
: null;
const resultRecord = result && typeof result === "object" && !Array.isArray(result)
? result as Record<string, unknown>
: null;
const chatSessionId = readString(argRecord, "chatSessionId") ?? readString(resultRecord, "chatSessionId") ?? null;
const laneId = readString(argRecord, "laneId") ?? readString(resultRecord, "laneId") ?? null;
emitProjectEvent(projectRoot, IPC.iosSimulatorEvent, {
type: "drawer-open-requested",
action,
mode,
chatSessionId,
laneId,
});
};
const iosSimulatorRpcService = {
...iosSimulatorService,
inspectPoint: async (arg: Parameters<typeof iosSimulatorService.inspectPoint>[0]) => {
const result = await iosSimulatorService.inspectPoint(arg);
requestIosSimulatorDrawerOpen("inspectPoint", arg, result);
return result;
},
launch: async (arg?: Parameters<typeof iosSimulatorService.launch>[0]) => {
const result = await iosSimulatorService.launch(arg);
requestIosSimulatorDrawerOpen("launch", arg, result);
return result;
},
openPreviewWorkspace: async (arg?: Parameters<typeof iosSimulatorService.openPreviewWorkspace>[0]) => {
Comment thread
greptile-apps[bot] marked this conversation as resolved.
const result = await iosSimulatorService.openPreviewWorkspace(arg);
requestIosSimulatorDrawerOpen("openPreviewWorkspace", arg, result);
return result;
},
renderPreview: async (arg: Parameters<typeof iosSimulatorService.renderPreview>[0]) => {
const result = await iosSimulatorService.renderPreview(arg);
requestIosSimulatorDrawerOpen("renderPreview", arg, result);
return result;
},
selectPoint: async (arg: Parameters<typeof iosSimulatorService.selectPoint>[0]) => {
requestIosSimulatorDrawerOpen("selectPoint", arg);
const result = await iosSimulatorService.selectPoint(arg);
requestIosSimulatorDrawerOpen("selectPoint", arg, result);
return result;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
},
startStream: async (arg?: Parameters<typeof iosSimulatorService.startStream>[0]) => {
const result = await iosSimulatorService.startStream(arg);
requestIosSimulatorDrawerOpen("startStream", arg, result);
return result;
},
tap: async (arg: Parameters<typeof iosSimulatorService.tap>[0]) => {
const result = await iosSimulatorService.tap(arg);
requestIosSimulatorDrawerOpen("tap", arg, result);
return result;
},
typeText: async (arg: Parameters<typeof iosSimulatorService.typeText>[0]) => {
const result = await iosSimulatorService.typeText(arg);
requestIosSimulatorDrawerOpen("typeText", arg, result);
return result;
},
drag: async (arg: Parameters<typeof iosSimulatorService.drag>[0]) => {
const result = await iosSimulatorService.drag(arg);
requestIosSimulatorDrawerOpen("drag", arg, result);
return result;
},
swipe: async (arg: Parameters<typeof iosSimulatorService.swipe>[0]) => {
const result = await iosSimulatorService.swipe(arg);
requestIosSimulatorDrawerOpen("swipe", arg, result);
return result;
},
};
const appControlService = createAppControlService({
projectRoot,
logger,
Expand Down Expand Up @@ -3470,7 +3542,7 @@ app.whenReady().then(async () => {
automationService,
automationPlannerService,
computerUseArtifactBrokerService,
iosSimulatorService,
iosSimulatorService: iosSimulatorRpcService,
appControlService,
builtInBrowserService,
orchestratorService,
Expand Down
22 changes: 22 additions & 0 deletions apps/desktop/src/main/rendererCsp.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { describe, expect, it } from "vitest";
import { buildRendererCspPolicy } from "./rendererCsp";

describe("buildRendererCspPolicy", () => {
it("allows packaged renderer fetches to local simulator stream URLs", () => {
const policy = buildRendererCspPolicy(false);

expect(policy).toContain("connect-src 'self' file: app: http://localhost:* http://127.0.0.1:* https:");
});

it("keeps dev websocket sources for Vite while allowing local fetches", () => {
const policy = buildRendererCspPolicy(true);

expect(policy).toContain("connect-src 'self' http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https:");
});

it("frames built-in browser content from local servers and about:blank in packaged builds", () => {
const policy = buildRendererCspPolicy(false);

expect(policy).toContain("frame-src 'self' file: app: http://localhost:* http://127.0.0.1:* about:");
});
});
23 changes: 23 additions & 0 deletions apps/desktop/src/main/rendererCsp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function buildRendererCspPolicy(isDevMode: boolean): string {
const cspSources = isDevMode
? "'self' http://localhost:* http://127.0.0.1:*"
: "'self' file: app:";
const cspWsSources = isDevMode ? " ws://localhost:* ws://127.0.0.1:*" : "";
const cspLocalSources = " http://localhost:* http://127.0.0.1:*";
const cspConnectLocalSources = isDevMode ? "" : cspLocalSources;
const cspImageSources = `${cspSources}${cspLocalSources} https://avatars.githubusercontent.com https://*.githubusercontent.com https://github.githubassets.com https://opengraph.githubassets.com https://github.com https://vercel.com https://*.vercel.com https://img.shields.io https://*.s3.amazonaws.com`;
return [
`default-src ${cspSources}`,
`base-uri 'self'`,
`form-action 'self'`,
`object-src 'none'`,
`frame-src ${cspSources}${cspLocalSources} about:`,
`script-src ${cspSources} 'unsafe-inline'`,
`style-src ${cspSources} 'unsafe-inline'`,
`img-src ${cspImageSources} ade-artifact: data: blob:`,
`media-src ${cspSources}${cspLocalSources} ade-artifact: blob: data:`,
`font-src ${cspSources} data:`,
`connect-src ${cspSources}${cspConnectLocalSources}${cspWsSources} https:`,
`worker-src 'self' blob:`,
].join("; ");
}
Loading
Loading