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
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
113 changes: 92 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,94 @@ 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]) => {
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 +3541,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