Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
26 changes: 1 addition & 25 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ import {
usageClientSurfaceFromRpcName,
} from "../../desktop/src/main/services/usage/usageStatsStore";
import { JsonRpcError, JsonRpcErrorCode, type JsonRpcHandler, type JsonRpcRequest } from "./jsonrpc";
import { normalizeAdeRuntimeRole } from "./runtimeRoles";
import { normalizeAdeRuntimeRole, resolveSessionRole } from "./runtimeRoles";
import { getSharedModelPickerStore } from "./services/modelPickerStore";
import { resolveLaneCreateRemoteBase } from "./services/laneCreateRemoteBase";
import { BUILT_IN_BROWSER_ACTOR_CAPABILITY_PARAM } from "./services/builtInBrowser/desktopBridgeMethods";
Expand Down Expand Up @@ -2952,30 +2952,6 @@ function isLocalComputerUseAllowed(callerCtx: CallerContext): boolean {
|| callerCtx.role === "agent";
}

function canDefaultRoleServeRequestedRole(
defaultRole: SessionIdentity["role"] | null,
requestedRole: SessionIdentity["role"],
): boolean {
if (requestedRole === "external") return true;
if (!defaultRole) return false;
if (defaultRole === "cto") return true;
if (defaultRole === "orchestrator") return requestedRole !== "cto";
if (defaultRole === "agent") return requestedRole === "agent";
if (defaultRole === "evaluator") return requestedRole === "evaluator";
return false;
}

function resolveSessionRole(
defaultRole: SessionIdentity["role"] | null,
requestedRole: SessionIdentity["role"] | null,
): SessionIdentity["role"] {
if (!defaultRole) return "external";
if (!requestedRole) return defaultRole;
return canDefaultRoleServeRequestedRole(defaultRole, requestedRole)
? requestedRole
: defaultRole;
}

async function listToolSpecsForSession(runtime: AdeRuntime, session: SessionState): Promise<ToolSpec[]> {
const callerCtx = await resolveEffectiveCallerContext(runtime, session);
const externalComputerUseAvailable = runtime.computerUseArtifactBrokerService
Expand Down
12 changes: 12 additions & 0 deletions apps/ade-cli/src/bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ import {
import { createLaneWorktreeLockService, type LaneWorktreeLockService } from "../../desktop/src/main/services/lanes/laneWorktreeLockService";
import { createHeadlessLinearServices } from "./headlessLinearServices";
import { EncryptedFileCredentialStore } from "./services/credentials/credentialStore";
import type { AccountAuthService } from "./services/account/accountAuthService";
import {
getSharedAccountAuthService,
registerAccountConfigProjectRoot,
} from "./services/account/sharedAccountAuthService";
import { createEventBuffer, type BufferedEvent, type EventBuffer } from "./eventBuffer";
import { readAutomationsEnvOverride } from "../../desktop/src/shared/automationAvailability";

Expand Down Expand Up @@ -244,6 +249,7 @@ export type AdeRuntime = {
linearIssueTracker?: ReturnType<typeof createLinearIssueTracker> | null;
processService?: ReturnType<typeof createProcessService> | null;
githubService?: ReturnType<typeof createGithubService> | null;
accountAuthService?: AccountAuthService | null;
automationService?: ReturnType<typeof createAutomationService> | null;
automationPlannerService?: ReturnType<typeof createAutomationPlannerService> | null;
computerUseArtifactBrokerService: ComputerUseArtifactBrokerService;
Expand Down Expand Up @@ -681,6 +687,11 @@ export async function createAdeRuntime(args: {
logger,
});
const projectSecretService = createProjectSecretService(projectRoot);
registerAccountConfigProjectRoot(projectRoot);
const accountAuthService = getSharedAccountAuthService({
projectRoots: () => [projectRoot],
logger,
});
const onboardingService = createOnboardingService({
db,
logger,
Expand Down Expand Up @@ -1707,6 +1718,7 @@ export async function createAdeRuntime(args: {
ctoMemoryService,
adeProjectService,
githubService: headlessLinearServices.githubService,
accountAuthService,
linearCredentialService: headlessLinearServices.linearCredentialService,
linearOAuthService,
prService: headlessLinearServices.prService,
Expand Down
129 changes: 129 additions & 0 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,73 @@ function writeSyncHostSingletonLock(args: {
}

describe("ADE CLI", () => {
it("builds projectless account commands and reports the signed-out local-first message", () => {
const statusPlan = expectExecutePlan(buildCliPlan(["auth", "status"]));
expect(statusPlan).toMatchObject({
label: "auth status",
formatter: "account-auth",
machineOnly: true,
machineAutoStart: true,
steps: [{
method: "account.call",
params: { action: "status", args: {} },
}],
});
expect(shouldAutoRegisterProjectForPlan(statusPlan)).toBe(false);

const logoutPlan = expectExecutePlan(buildCliPlan(["logout"]));
expect(logoutPlan.steps[0]).toMatchObject({
method: "account.call",
params: { action: "signOut", args: {} },
});
expect(shouldAutoRegisterProjectForPlan(logoutPlan)).toBe(false);

expect(buildCliPlan(["login", "--max-wait", "42"])).toEqual({
kind: "account-login",
maxWaitSec: 42,
});
const rawActionPlan = expectExecutePlan(buildCliPlan(["actions", "run", "account.status"]));
expect(rawActionPlan.steps[0]).toMatchObject({
method: "account.call",
params: { action: "status", args: {} },
});

const connection = {
mode: "runtime-socket" as const,
projectRoot: "/unused",
workspaceRoot: "/unused",
socketPath: "/tmp/ade.sock",
request: async () => null,
close: () => {},
};
const summarized = summarizeExecution({
plan: statusPlan,
connection,
values: {
result: {
domain: "account",
action: "status",
result: {
signedIn: false,
userId: null,
email: null,
name: null,
expiresAt: null,
},
statusHints: {},
},
},
});
expect(formatOutput(summarized, {
...baseResolveOpts(),
projectRoot: null,
workspaceRoot: null,
text: true,
}, inferFormatter(statusPlan))).toBe(
"Not signed in — local use does not require an account.\n",
);
});

it("parses global options without stealing command flags", () => {
const parsed = parseCliArgs([
"--project-root",
Expand Down Expand Up @@ -2619,6 +2686,68 @@ describe("ADE CLI", () => {
}
});

posixIt("reports signed-out account status over the machine socket in headless mode", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-account-status-sock-"));
const socketPath = path.join(root, "ade.sock");
const requests: Array<{ method: string; params?: unknown }> = [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the duplicate requests declaration.

The identical const requests declaration appears twice in the same scope, causing a compile-time redeclaration error.

 const requests: Array<{ method: string; params?: unknown }> = [];
-const requests: Array<{ method: string; params?: unknown }> = [];
🧰 Tools
🪛 ast-grep (0.44.1)

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: import { spawn } from "node:child_process";
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process-typescript)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/cli.test.ts` at line 2692, Remove the duplicate const
requests declaration in the test scope, keeping a single requests variable for
the surrounding test logic.

const stop = await startHeadlessRpcSocketServer({
socketPath,
createHandler: () => (async (request: any) => {
requests.push({ method: request.method, params: request.params });
if (request.method === "ade/initialize") {
return {
runtimeInfo: {
version: process.env.ADE_CLI_VERSION?.trim() || "0.0.0",
buildHash: null,
defaultRole: "cto",
packageChannel: null,
projectRoot: null,
pid: process.pid,
},
};
}
if (request.method === "account.call") {
return {
domain: "account",
action: "status",
result: {
signedIn: false,
userId: null,
email: null,
name: null,
expiresAt: null,
},
statusHints: {},
};
}
throw new Error(`Unexpected method: ${request.method}`);
}) as any,
});

try {
const result = await runCli([
"--socket",
socketPath,
"--headless",
"auth",
"status",
"--text",
]);
expect(result).toEqual({
output: "Not signed in — local use does not require an account.\n",
exitCode: 0,
});
expect(requests.at(-1)).toEqual({
method: "account.call",
params: { action: "status", args: {} },
});
expect(requests.some((request) => request.method === "projects.add")).toBe(false);
} finally {
stop?.();
fs.rmSync(root, { recursive: true, force: true });
}
});

posixIt("advises starting the machine brain when a personal chat connection fails", async () => {
const socketPath = path.join(
fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-personal-chat-missing-")),
Expand Down
Loading