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
2 changes: 0 additions & 2 deletions .ade/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
# Shared ADE project config
!.gitignore
!ade.yaml
!cto/
!cto/identity.yaml

# Shared user-authored ADE assets
!templates/
Expand Down
13 changes: 0 additions & 13 deletions .ade/cto/identity.yaml

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
node_modules
/.npm-cache
/apps/desktop/release-stable
/apps/desktop/release-logs
/apps/desktop/.cache

# Temp/debug artifacts
Expand Down
4 changes: 4 additions & 0 deletions apps/ade-cli/scripts/build-static.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,10 @@ async function main() {
nativeArchivePath = path.join(args.outDir, `ade-${args.target}.native.tar.gz`);
}

if (process.env.ADE_KEEP_STATIC_RUNTIME_STAGING !== "1") {
await fs.rm(workDir, { recursive: true, force: true });
}

process.stdout.write(`${JSON.stringify({
target: args.target,
binaryPath,
Expand Down
102 changes: 92 additions & 10 deletions apps/ade-cli/scripts/notarize-static-runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ async function assertExists(filePath, label) {
}
}

async function pathExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}

async function run(command, args, options = {}) {
const result = await execFileAsync(command, args, {
maxBuffer: 10 * 1024 * 1024,
Expand All @@ -48,6 +57,87 @@ async function findDeveloperIdIdentity() {
throw new Error("Unable to find a Developer ID Application signing identity.");
}

async function walkFiles(rootPath, files = []) {
const entries = await fs.readdir(rootPath, { withFileTypes: true });
for (const entry of entries) {
const entryPath = path.join(rootPath, entry.name);
if (entry.isDirectory()) {
await walkFiles(entryPath, files);
} else if (entry.isFile()) {
files.push(entryPath);
}
}
return files;
}

async function isMachO(filePath) {
const handle = await fs.open(filePath, "r");
try {
const buffer = Buffer.alloc(4);
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
if (bytesRead < 4) return false;
return [
"feedface",
"feedfacf",
"cefaedfe",
"cffaedfe",
"cafebabe",
"bebafeca",
"cafebabf",
"bfbafeca",
].includes(buffer.toString("hex"));
} finally {
await handle.close();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

async function signBinary(binaryPath, identity) {
await run("codesign", [
"--force",
"--options",
"runtime",
"--timestamp",
"--sign",
identity,
binaryPath,
]);
await run("codesign", ["--verify", "--strict", "--verbose=4", binaryPath]);
}

async function signNativeArchiveIfPresent(binaryPath, identity) {
const archivePath = `${binaryPath}.native.tar.gz`;
if (!(await pathExists(archivePath))) {
return;
}

const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "ade-runtime-native-sign-"));
try {
console.log(`[runtime:notarize] Signing Mach-O payloads in ${archivePath}`);
await run("tar", ["-xzf", archivePath, "-C", workDir]);

const files = await walkFiles(workDir);
let signed = 0;
for (const filePath of files) {
if (!(await isMachO(filePath))) continue;
await signBinary(filePath, identity);
signed += 1;
}

if (signed === 0) {
console.log(`[runtime:notarize] No Mach-O payloads found in ${path.basename(archivePath)}`);
} else {
console.log(`[runtime:notarize] Signed ${signed} Mach-O payload(s) in ${path.basename(archivePath)}`);
}

const nextArchivePath = `${archivePath}.tmp`;
await fs.rm(nextArchivePath, { force: true });
await run("tar", ["-czf", nextArchivePath, "-C", workDir, "."]);
await fs.rename(nextArchivePath, archivePath);
} finally {
await fs.rm(workDir, { recursive: true, force: true });
}
}

function buildNotarytoolArgs(zipPath) {
if (hasEnv("APPLE_API_KEY") && hasEnv("APPLE_API_KEY_ID") && hasEnv("APPLE_API_ISSUER")) {
return [
Expand Down Expand Up @@ -105,16 +195,8 @@ if (process.platform !== "darwin") {

const identity = await findDeveloperIdIdentity();
console.log(`[runtime:notarize] Signing ${binaryPath} with ${identity}`);
await run("codesign", [
"--force",
"--options",
"runtime",
"--timestamp",
"--sign",
identity,
binaryPath,
]);
await run("codesign", ["--verify", "--strict", "--verbose=4", binaryPath]);
await signBinary(binaryPath, identity);
await signNativeArchiveIfPresent(binaryPath, identity);

const workDir = await fs.mkdtemp(path.join(os.tmpdir(), "ade-runtime-notary-"));
const zipPath = path.join(workDir, `${path.basename(binaryPath)}.zip`);
Expand Down
80 changes: 76 additions & 4 deletions apps/ade-cli/scripts/package-native-deps.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,70 @@ function isOpenCodePlatformPackage(packageName) {
return /^opencode-(?:darwin|linux|windows)-/.test(packageName);
}

function targetParts(target) {
const [platform, arch] = target.split("-");
return { platform, arch };
}

function platformPackageTarget(packageName) {
const patterns = [
/^@openai\/codex-(darwin|linux|win32)-(arm64|x64)$/,
/^@cursor\/sdk-(darwin|linux|win32)-(arm64|x64)$/,
/^@anthropic-ai\/claude-agent-sdk-(darwin|linux)-(arm64|x64)(?:-musl)?$/,
/^opencode-(darwin|linux|windows)-(arm64|x64)$/,
/^@esbuild\/(darwin|linux|win32)-(arm64|x64)$/,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
];

for (const pattern of patterns) {
const match = pattern.exec(packageName);
if (match) return { platform: match[1], arch: match[2] };
}
return null;
}

function isPackageForOtherTarget(packageName, target) {
const packageTarget = platformPackageTarget(packageName);
if (!packageTarget) return false;
const targetPlatform = packageTarget.platform === "win32" || packageTarget.platform === "windows"
? "windows"
: packageTarget.platform;
const { platform, arch } = targetParts(target);
return targetPlatform !== platform || packageTarget.arch !== arch;
}

function nodePtyPrebuildTarget(target) {
const { platform, arch } = targetParts(target);
if (platform === "darwin") return `darwin-${arch}`;
if (platform === "linux") return `linux-${arch}`;
return target;
}

function shouldCopyPackageEntry(packageName, sourceRoot, entry, target) {
const relative = path.relative(sourceRoot, entry).split(path.sep).join("/");
if (!relative || relative.startsWith("..")) return true;

if (packageName === "node-pty") {
if (relative.startsWith("prebuilds/")) {
// The fs.cp filter receives the target directory entry itself
// (e.g. "prebuilds/darwin-arm64") with no trailing slash. Returning
// false for that entry would skip the ENTIRE subtree (pty.node +
// spawn-helper), shipping an empty prebuilds/ and breaking PTY in the
// remote runtime. Match the exact dir name as well as its contents.
const prebuildDir = `prebuilds/${nodePtyPrebuildTarget(target)}`;
return relative === "prebuilds" || relative === prebuildDir || relative.startsWith(`${prebuildDir}/`);
}
if (relative.startsWith("build/")) {
return target.startsWith("linux-");
}
}

if (packageName === "opencode-ai" && relative === "bin/opencode.exe") {
return false;
}

return true;
}

async function collectRuntimePackages(target) {
const rootManifest = await readJson(path.join(packageRoot, "package.json"));
const platformCursorPackage = `@cursor/sdk-${target}`;
Expand All @@ -87,6 +151,7 @@ async function collectRuntimePackages(target) {
const packageName = queue.shift();
if (!packageName || visited.has(packageName)) continue;
if (isOpenCodePlatformPackage(packageName)) continue;
if (isPackageForOtherTarget(packageName, target)) continue;
visited.add(packageName);
const manifest = await readPackageManifest(packageName);
if (!manifest) continue;
Expand All @@ -103,14 +168,17 @@ async function collectRuntimePackages(target) {
if (isOpenCodePlatformPackage(dependencyName)) {
continue;
}
if (isPackageForOtherTarget(dependencyName, target)) {
continue;
}
if (!visited.has(dependencyName)) queue.push(dependencyName);
}
}

return packages.sort((a, b) => a.localeCompare(b));
}

async function copyPackage(packageName, destinationRoot) {
async function copyPackage(packageName, destinationRoot, target) {
const source = packagePath(packageName);
if (!(await exists(source))) return false;
const destination = path.join(destinationRoot, "node_modules", ...packageName.split("/"));
Expand All @@ -123,7 +191,8 @@ async function copyPackage(packageName, destinationRoot) {
return !normalized.includes("/.cache/")
&& !normalized.includes("/test/")
&& !normalized.includes("/tests/")
&& !normalized.endsWith(".map");
&& !normalized.endsWith(".map")
&& shouldCopyPackageEntry(packageName, source, entry, target);
},
});
return true;
Expand Down Expand Up @@ -187,7 +256,7 @@ async function main() {
const packageNames = await collectRuntimePackages(args.target);
const copied = [];
for (const packageName of packageNames) {
if (await copyPackage(packageName, bundleRoot)) {
if (await copyPackage(packageName, bundleRoot, args.target)) {
copied.push(packageName);
}
}
Expand All @@ -196,10 +265,13 @@ async function main() {

const archivePath = path.join(args.outDir, `ade-${args.target}.native.tar.gz`);
await makeTarGz(bundleRoot, archivePath);
if (process.env.ADE_KEEP_NATIVE_RUNTIME_STAGING !== "1") {
await fs.rm(bundleRoot, { recursive: true, force: true });
}
process.stdout.write(`${JSON.stringify({
target: args.target,
archivePath,
bundleRoot,
bundleRoot: process.env.ADE_KEEP_NATIVE_RUNTIME_STAGING === "1" ? bundleRoot : null,
packages: copied,
}, null, 2)}\n`);
}
Expand Down
62 changes: 59 additions & 3 deletions apps/ade-cli/src/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4746,7 +4746,10 @@ describe("ADE CLI", () => {
});
});

it("browser commands map to built-in browser actions", () => {
it("browser commands map to built-in browser actions", () => withEnv({
ADE_LANE_ID: undefined,
ADE_CHAT_SESSION_ID: undefined,
}, () => {
const open = buildCliPlan([
"browser",
"open",
Expand Down Expand Up @@ -5207,7 +5210,7 @@ describe("ADE CLI", () => {
],
},
});
});
}));

it("browser open and claim commands carry the agent lane claim", () => {
const previousLane = process.env.ADE_LANE_ID;
Expand All @@ -5225,13 +5228,51 @@ describe("ADE CLI", () => {
action: "navigate",
args: {
url: "localhost:5173",
activate: false,
reuseOwnedTab: true,
openPanel: false,
laneId: "lane-env-1",
chatSessionId: "chat-env-1",
},
},
});

const newTabOpen = buildCliPlan(["browser", "open", "localhost:5173", "--new-tab"]);
expect(newTabOpen.kind).toBe("execute");
if (newTabOpen.kind !== "execute") return;
expect(newTabOpen.steps[0]?.params).toMatchObject({
arguments: {
domain: "built_in_browser",
action: "navigate",
args: {
url: "localhost:5173",
activate: false,
newTab: true,
openPanel: false,
laneId: "lane-env-1",
chatSessionId: "chat-env-1",
},
},
});
expect((newTabOpen.steps[0]?.params as any).arguments.args.reuseOwnedTab).toBeUndefined();

const panelOpen = buildCliPlan(["browser", "open", "localhost:5173", "--panel"]);
expect(panelOpen.kind).toBe("execute");
if (panelOpen.kind !== "execute") return;
expect(panelOpen.steps[0]?.params).toMatchObject({
arguments: {
domain: "built_in_browser",
action: "navigate",
args: {
url: "localhost:5173",
reuseOwnedTab: true,
openPanel: true,
laneId: "lane-env-1",
chatSessionId: "chat-env-1",
},
},
});
expect((panelOpen.steps[0]?.params as any).arguments.args.activate).toBeUndefined();

const activeOpen = buildCliPlan(["browser", "open", "localhost:5173", "--active-tab"]);
expect(activeOpen.kind).toBe("execute");
Expand All @@ -5242,13 +5283,28 @@ describe("ADE CLI", () => {
action: "navigate",
args: {
url: "localhost:5173",
openPanel: true,
openPanel: false,
laneId: "lane-env-1",
chatSessionId: "chat-env-1",
},
},
});
expect((activeOpen.steps[0]?.params as any).arguments.args.newTab).toBeUndefined();
expect((activeOpen.steps[0]?.params as any).arguments.args.activate).toBeUndefined();

const ownedScreenshot = buildCliPlan(["browser", "screenshot"]);
expect(ownedScreenshot.kind).toBe("execute");
if (ownedScreenshot.kind !== "execute") return;
expect(ownedScreenshot.steps[0]?.params).toMatchObject({
arguments: {
domain: "built_in_browser",
action: "captureScreenshot",
args: {
laneId: "lane-env-1",
chatSessionId: "chat-env-1",
},
},
});

const panel = buildCliPlan(["browser", "panel"]);
expect(panel.kind).toBe("execute");
Expand Down
Loading