mess around with a rust agent version with just rev shell.

This commit is contained in:
KingKDot
2026-08-19 09:54:52 -05:00
parent 691e1c3ba4
commit 7a8fcf39ec
47 changed files with 4790 additions and 93 deletions
+2 -1
View File
@@ -40,8 +40,9 @@ Use `runtime: "native"` for agent-side plugins that need direct OS APIs, native
| Directory | Purpose |
|-----------|---------|
| `rust-lite-builder` | Server-side custom provider that builds the authenticated Rust Lite agent. |
| `sample-build-hooks` | Server-only build plugin with its own Build page button and artifact replacement example. |
| `sample-ts-fullstack` | TypeScript UI plus TypeScript server runtime. |
| `sample-c`, `sample-cpp`, `sample-go`, `sample-rust` | Native agent plugin examples. |
Start with [docs/server-side-plugins.md](docs/server-side-plugins.md) if you are building uploaders, custom build buttons, artifact post-processing, or internal release hooks.
Start with [docs/server-side-plugins.md](docs/server-side-plugins.md) if you are building custom agent providers, uploaders, build buttons, artifact post-processing, or internal release hooks.
+2 -1
View File
@@ -4,6 +4,7 @@ The `plugins/` directory includes small examples that exercise each plugin style
| Directory | Demonstrates |
|-----------|--------------|
| `rust-lite-builder` | Custom server-side build provider that compiles the Rust Lite agent through the standard builder lifecycle. |
| `sample-build-hooks` | Server-only build plugin, custom Build page action, build settings, and replacing the produced artifact with a `.txt` file. |
| `sample-ts-fullstack` | TypeScript UI and TypeScript server runtime with shared local modules. |
| `sample-c` | Native C plugin for the smallest unloadable ABI surface. |
@@ -11,4 +12,4 @@ The `plugins/` directory includes small examples that exercise each plugin style
| `sample-go` | Native Go plugin. Go plugins are intentionally not unloaded because the Go runtime cannot be safely unloaded from shared libraries. |
| `sample-rust` | Native Rust plugin with C ABI exports and unload-friendly runtime handling. |
Use `sample-build-hooks` as the starting point for uploaders, artifact publishers, or custom release buttons.
Use `rust-lite-builder` as the starting point for a fully custom agent compiler. Use `sample-build-hooks` for uploaders, artifact publishers, or custom release buttons around the built-in agent.
+49
View File
@@ -297,6 +297,55 @@ Buttons in `build.actions` can set core builder fields through `setBuild` and pl
See `plugins/sample-build-hooks` for a minimal server-only build plugin with its own build button and artifact replacement.
## Custom Build Providers
A server-only plugin can replace the built-in Go compilation step while keeping the normal Build page, permissions, audit log, progress stream, artifact hooks, file-share upload, build history, and expiration handling. Declare the provider and export `onBuild`:
```json
{
"runtime": "server",
"build": {
"provider": {
"label": "My Lite Agent",
"description": "Builds the reduced agent implementation.",
"icon": "fa-solid fa-feather",
"platforms": ["windows-amd64", "linux-amd64"]
}
}
}
```
```js
export default {
async onBuild(ctx, payload) {
ctx.build.status("Compiling lite agent...");
ctx.build.output("Compiler started", "info");
// Write only inside payload.outputDir.
const filename = `lite-${payload.platforms[0]}`;
await compileAgent({ output: `${payload.outputDir}/${filename}` });
return {
artifacts: [{
name: "Lite Agent",
filename,
platform: payload.platforms[0],
version: "1.0.0"
}]
};
}
};
```
The provider payload contains `buildId`, `providerId`, `runtimeRoot`, a temporary `outputDir`, selected `platforms`, `serverUrl`, `agentToken`, signed `buildTag`, `tlsSpkiPins`, `initialClientTag`, `outputName`, provider `settings`, and the sanitized full build `config`.
An artifact entry must contain a selected `platform` and either `filename` or a relative `path` beneath `outputDir`. Optional fields are `name` and `version`. Provider output is limited to 32 regular, non-symlink files of at most 512 MB each. The server validates containment, copies approved files into `dist-clients`, adds the build ID suffix, then invokes normal artifact and completion hooks.
`ctx.build.output(text, level)` accepts `debug`, `info`, `success`, `warn`, or `error`. `ctx.build.status(text, progress?, etaSeconds?)` changes the Build page status line and can display a progress bar and estimated time remaining. Progress is a number from 0 through 100. These methods exist only during `onBuild`.
The selected provider must be enabled, include server code, support every requested target, and be accessible to the building user. A provider plugin is forced on for its own build, even if a modified request marks its hook settings as disabled.
See `plugins/rust-lite-builder` for a complete provider that invokes Cargo and embeds authenticated connection defaults into Overlord Lite.
## RBAC And Build Plugin Access
Build-hook plugins use the normal per-user plugin access policy. Admins can open a user in the Users page, choose Plugin Access, and set one of these modes:
+41
View File
@@ -0,0 +1,41 @@
# Rust Lite Builder Plugin
This trusted server-only plugin registers Rust Lite as an agent build provider.
Select **Rust Lite** from the Builder's **Agent Type** field, choose targets, and
start the build normally. Output, history, downloads, expiration, rate limits,
RBAC, and artifact hooks remain owned by the Overlord server.
The resulting client supports authenticated enrollment, TLS identity pinning,
heartbeat, reconnect behavior, and interactive TTY console sessions. It
intentionally contains no plugin loader or other remote command handlers.
During compilation the Build page shows estimated overall/target percentages,
completed Cargo crates, elapsed time, the current compile or link/LTO phase,
and a rolling ETA. Cargo does not expose exact link progress, so percentages
and ETA are estimates. Completed target durations are retained in plugin data
to improve later estimates, and the persistent Cargo target directory makes
subsequent builds substantially faster.
The **Cargo Jobs** setting defaults to `0`, which leaves parallelism at Cargo's
host-CPU default. Set a positive number only when the server needs an explicit
resource cap. The plugin cache is separate from `Overlord-Lite/target`, so the
first plugin build does not reuse artifacts from a manual source-tree build.
The server host needs Rust 1.88 or newer. Native-host builds work with the
standard Rust installation. Cross-target builds additionally require the target
from `rustup target add <triple>` and a compatible linker/toolchain.
The plugin locates `Overlord-Lite` beside the server runtime or under its
`dist/` directory. Production package scripts copy that source automatically.
Builder releases compile in the selected server URL, agent token, and signed
build tag. Runtime `OVERLORD_SERVER`, `OVERLORD_AGENT_TOKEN`, and
`OVERLORD_BUILD_TAG` variables can still override those defaults.
Builds also embed the server's active TLS SPKI pins. This lets Lite clients
trust the server's generated self-signed certificate without disabling
certificate verification and protects later HTTPS plugin downloads too.
All provider builds use Cargo's `--release` profile. That profile uses
`opt-level = "z"`, full LTO, one codegen unit, symbol stripping, and
`panic = "abort"`.
+8
View File
@@ -0,0 +1,8 @@
@echo off
setlocal
set "PLUGIN_ID=rust-lite-builder"
set "OUT=%~dp0%PLUGIN_ID%.zip"
if exist "%OUT%" del /q "%OUT%"
powershell -NoProfile -ExecutionPolicy Bypass -Command "Compress-Archive -Path '%~dp0config.json','%~dp0rust-lite-builder.html','%~dp0rust-lite-builder.css','%~dp0rust-lite-builder.js','%~dp0server.js' -DestinationPath '%OUT%' -Force"
if errorlevel 1 exit /b 1
echo [ok] %OUT%
+7
View File
@@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT"
rm -f rust-lite-builder.zip
zip -q rust-lite-builder.zip config.json rust-lite-builder.html rust-lite-builder.css rust-lite-builder.js server.js
echo "[ok] $ROOT/rust-lite-builder.zip"
+58
View File
@@ -0,0 +1,58 @@
{
"name": "Rust Lite Builder",
"version": "1.2.0",
"apiVersion": 2,
"runtime": "server",
"description": "Adds a dedicated builder for the authenticated Rust Lite agent with an interactive TTY console.",
"build": {
"label": "Rust Lite Builder",
"description": "Builds the lightweight Rust client instead of the full Go agent.",
"enabledByDefault": true,
"provider": {
"label": "Rust Lite",
"description": "Authenticated sessions, TLS pinning, heartbeat, and an interactive TTY console.",
"icon": "fa-brands fa-rust",
"platforms": [
"windows-amd64",
"windows-arm64",
"linux-amd64",
"linux-arm64",
"darwin-amd64",
"darwin-arm64"
]
},
"settings": [
{
"key": "locked",
"label": "Require Cargo.lock",
"type": "boolean",
"default": true,
"description": "Build with --locked for reproducible dependency resolution."
},
{
"key": "jobs",
"label": "Cargo Jobs",
"type": "number",
"default": 0,
"min": 0,
"max": 32,
"description": "Parallel Cargo jobs. Use 0 to let Cargo use the host's available CPUs."
}
],
"actions": [
{
"id": "use-rust-lite",
"label": "Use Rust Lite Builder",
"icon": "fa-brands fa-rust",
"description": "Selects Rust Lite as the active agent builder.",
"setBuild": {
"buildProvider": "rust-lite-builder"
}
}
]
},
"navbar": {
"label": "Rust Lite",
"icon": "fa-brands fa-rust"
}
}
@@ -0,0 +1,49 @@
.rust-lite-builder {
display: grid;
gap: 1rem;
color: #e2e8f0;
}
.rust-lite-builder header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
}
.rust-lite-builder h2 {
margin: 0.15rem 0 0.35rem;
font-size: 1.5rem;
}
.rust-lite-builder p {
margin: 0;
color: #94a3b8;
}
.rust-lite-builder .eyebrow {
color: #fb923c;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.rust-lite-builder button {
border: 1px solid #c2410c;
border-radius: 0.5rem;
background: #9a3412;
color: white;
cursor: pointer;
padding: 0.55rem 0.8rem;
}
.rust-lite-builder pre {
min-height: 8rem;
overflow: auto;
border: 1px solid #334155;
border-radius: 0.65rem;
background: #0f172a;
padding: 1rem;
white-space: pre-wrap;
}
@@ -0,0 +1,11 @@
<main class="rust-lite-builder">
<header>
<div>
<p class="eyebrow">Server build provider</p>
<h2>Rust Lite Builder</h2>
<p>Builds the authenticated Rust agent with interactive TTY console support through the normal Overlord build workflow.</p>
</div>
<button id="rust-lite-refresh" type="button">Refresh status</button>
</header>
<pre id="rust-lite-status">Loading...</pre>
</main>
@@ -0,0 +1,21 @@
const statusOutput = document.getElementById("rust-lite-status");
const refreshButton = document.getElementById("rust-lite-refresh");
async function refreshStatus() {
statusOutput.textContent = "Checking Rust Lite builder...";
try {
const response = await fetch("/api/plugins/rust-lite-builder/rpc", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ method: "status", params: {} }),
});
const body = await response.json();
if (!body.ok) throw new Error(body.error || "Status check failed");
statusOutput.textContent = JSON.stringify(body.result, null, 2);
} catch (error) {
statusOutput.textContent = `Error: ${error?.message || error}`;
}
}
refreshButton?.addEventListener("click", refreshStatus);
refreshStatus();
+415
View File
@@ -0,0 +1,415 @@
import fs from "fs";
import path from "path";
const TARGETS = {
"windows-amd64": { windows: "x86_64-pc-windows-msvc", other: "x86_64-pc-windows-gnu", extension: ".exe" },
"windows-arm64": { windows: "aarch64-pc-windows-msvc", other: "aarch64-pc-windows-gnullvm", extension: ".exe" },
"linux-amd64": { all: "x86_64-unknown-linux-gnu", extension: "" },
"linux-arm64": { all: "aarch64-unknown-linux-gnu", extension: "" },
"darwin-amd64": { all: "x86_64-apple-darwin", extension: "" },
"darwin-arm64": { all: "aarch64-apple-darwin", extension: "" },
};
function rustTarget(platform) {
const target = TARGETS[platform];
if (!target) throw new Error(`Unsupported Rust Lite platform: ${platform}`);
return process.platform === "win32" ? (target.windows || target.all) : (target.other || target.all);
}
function findSource(runtimeRoot) {
const candidates = [
path.join(runtimeRoot, "Overlord-Lite"),
path.join(runtimeRoot, "dist", "Overlord-Lite"),
path.join(runtimeRoot, "..", "Overlord-Lite"),
];
for (const candidate of candidates) {
if (fs.existsSync(path.join(candidate, "Cargo.toml"))) return candidate;
}
throw new Error(`Overlord-Lite source not found. Checked: ${candidates.join(", ")}`);
}
async function capture(command, cwd) {
const proc = Bun.spawn(command, { cwd, stdout: "pipe", stderr: "pipe" });
const [stdout, stderr, exitCode] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text(),
proc.exited,
]);
if (exitCode !== 0) throw new Error(stderr.trim() || `${command[0]} exited with ${exitCode}`);
return stdout.trim();
}
async function streamPipe(stream, onLine) {
const reader = stream.getReader();
const decoder = new TextDecoder();
let pending = "";
while (true) {
const { done, value } = await reader.read();
pending += decoder.decode(value || new Uint8Array(), { stream: !done });
const lines = pending.split(/\r?\n/);
pending = lines.pop() || "";
for (const line of lines) if (line.trim()) onLine(line);
if (done) break;
}
if (pending.trim()) onLine(pending);
}
function packageVersion(sourceDir) {
const cargo = fs.readFileSync(path.join(sourceDir, "Cargo.toml"), "utf8");
return cargo.match(/^version\s*=\s*"([^"]+)"/m)?.[1] || "unknown";
}
function formatDuration(milliseconds) {
const seconds = Math.max(0, Math.round(milliseconds / 1000));
if (seconds < 60) return `${seconds}s`;
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (minutes < 60) return remainingSeconds ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return remainingMinutes ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
}
async function estimatePackageCount(sourceDir, target, locked) {
const args = [
"cargo",
"tree",
"--target",
target,
"--prefix",
"none",
"--format",
"{p}",
];
if (locked) args.push("--locked");
const output = await capture(args, sourceDir);
const packages = new Set(
output
.split(/\r?\n/)
.map((line) => line.replace(/ \(\*\)$/, "").replace(/ \(proc-macro\)$/, "").trim())
.filter(Boolean),
);
return packages.size;
}
function readBuildHistory(dataDir) {
try {
const parsed = JSON.parse(fs.readFileSync(path.join(dataDir, "build-history.json"), "utf8"));
return parsed && typeof parsed === "object" ? parsed : {};
} catch {
return {};
}
}
function recordBuildDuration(dataDir, target, durationMs) {
const historyPath = path.join(dataDir, "build-history.json");
const history = readBuildHistory(dataDir);
const previous = Number(history[target]?.durationMs);
const samples = Math.max(0, Number(history[target]?.samples) || 0) + 1;
history[target] = {
durationMs: Number.isFinite(previous)
? Math.round(previous * 0.6 + durationMs * 0.4)
: Math.round(durationMs),
samples,
updatedAt: Date.now(),
};
try {
fs.writeFileSync(historyPath, JSON.stringify(history, null, 2));
} catch {
// Progress history is optional; a read-only data directory must not fail the build.
}
}
function createTargetProgress(ctx, options) {
const DEFAULT_LINK_BUDGET_MS = 35_000;
const startedAt = Date.now();
const packageStates = new Map();
let linking = false;
let linkingStartedAt = null;
let finalizing = false;
let finished = false;
let lastStatusAt = 0;
let lastLoggedBucket = -1;
function snapshot() {
const elapsedMs = Date.now() - startedAt;
const completedPackages = packageStates.size;
const freshPackages = Array.from(packageStates.values()).filter(Boolean).length;
const packageRatio = options.estimatedPackages > 0
? Math.min(1, completedPackages / options.estimatedPackages)
: 0;
const historicalDurationMs = Number.isFinite(options.historicalDurationMs)
? options.historicalDurationMs
: null;
const linkBudgetMs = historicalDurationMs === null
? DEFAULT_LINK_BUDGET_MS
: Math.max(10_000, historicalDurationMs * 0.4);
const linkingElapsedMs = linkingStartedAt === null ? 0 : Date.now() - linkingStartedAt;
let targetRatio = Math.min(0.7, packageRatio * 0.7);
if (linking) {
const estimatedLinkRatio = Math.min(0.95, linkingElapsedMs / linkBudgetMs);
targetRatio = Math.max(targetRatio, 0.7 + estimatedLinkRatio * 0.28);
}
if (finalizing) targetRatio = Math.max(targetRatio, 0.98);
if (finished) targetRatio = 1;
const overallRatio = (options.targetIndex + targetRatio) / options.targetCount;
let predictedTargetMs = null;
if (historicalDurationMs !== null && historicalDurationMs > 0) {
predictedTargetMs = Math.max(elapsedMs, historicalDurationMs);
}
if (linking && historicalDurationMs === null) {
predictedTargetMs = elapsedMs + Math.max(5_000, linkBudgetMs - linkingElapsedMs);
} else if (!linking && elapsedMs >= 5_000 && packageRatio >= 0.05) {
const rollingPrediction = elapsedMs / packageRatio + linkBudgetMs;
predictedTargetMs = predictedTargetMs === null
? rollingPrediction
: predictedTargetMs * 0.65 + rollingPrediction * 0.35;
}
const etaMs = predictedTargetMs === null || finished
? null
: Math.max(
0,
predictedTargetMs - elapsedMs +
(options.targetCount - options.targetIndex - 1) * predictedTargetMs,
);
return {
elapsedMs,
completedPackages,
freshPackages,
targetRatio,
overallRatio,
etaMs,
};
}
function emit(force = false) {
const now = Date.now();
if (!force && now - lastStatusAt < 1_500) return;
lastStatusAt = now;
const state = snapshot();
const targetPercent = Math.round(state.targetRatio * 100);
const overallPercent = Math.round(state.overallRatio * 100);
const phase = finalizing
? "finalizing artifact"
: linking
? "linking + LTO"
: "compiling";
const pieces = [
`Rust Lite ${options.platform} (${options.targetIndex + 1}/${options.targetCount})`,
`${overallPercent}% overall`,
`${targetPercent}% target`,
phase,
];
if (options.estimatedPackages > 0) {
pieces.push(`${state.completedPackages}/${options.estimatedPackages} crates`);
}
pieces.push(`${formatDuration(state.elapsedMs)} elapsed`);
if (state.etaMs !== null && state.etaMs >= 1_000) {
pieces.push(`ETA ~${formatDuration(state.etaMs)}`);
}
const text = pieces.join(" • ");
ctx.build.status(
text,
Math.min(100, state.overallRatio * 100),
state.etaMs === null ? undefined : Math.ceil(state.etaMs / 1000),
);
const bucket = Math.floor(overallPercent / 10);
if (bucket > lastLoggedBucket && overallPercent > 0) {
lastLoggedBucket = bucket;
ctx.build.output(`Progress: ${text}`);
}
}
return {
startedAt,
noteArtifact(packageId, fresh) {
const previous = packageStates.get(packageId);
packageStates.set(packageId, previous === false ? false : fresh === true);
emit();
},
markLinking() {
linking = true;
linkingStartedAt ??= Date.now();
emit(true);
},
markFinalizing() {
finalizing = true;
emit(true);
},
markFinished() {
finished = true;
emit(true);
return snapshot();
},
emit,
};
}
export default {
setup(ctx) {
ctx.log.info("Rust Lite build provider ready");
},
async onBuild(ctx, payload) {
const sourceDir = findSource(String(payload.runtimeRoot || ""));
const outputDir = path.resolve(String(payload.outputDir || ""));
const targetDir = path.join(ctx.dataDir, "cargo-target");
const platforms = Array.isArray(payload.platforms) ? payload.platforms : [];
if (platforms.length === 0) throw new Error("No Rust Lite platforms were requested");
fs.mkdirSync(outputDir, { recursive: true });
fs.mkdirSync(targetDir, { recursive: true });
const cargoVersion = await capture(["cargo", "--version"], sourceDir);
ctx.build.output(`Using ${cargoVersion}`);
ctx.build.output(`Rust Lite source: ${sourceDir}`);
const settings = payload.settings && typeof payload.settings === "object" ? payload.settings : {};
const tlsSpkiPins = Array.isArray(payload.tlsSpkiPins)
? payload.tlsSpkiPins.map(String).filter(Boolean)
: [];
if (tlsSpkiPins.length > 0) {
ctx.build.output(
`TLS identity pinning: enabled (${tlsSpkiPins.length} trusted key${tlsSpkiPins.length === 1 ? "" : "s"})`,
);
} else {
ctx.build.output("WARNING: No TLS SPKI pin is available for this build", "warn");
}
const requestedJobs = Math.trunc(Number(settings.jobs));
const jobs = Number.isFinite(requestedJobs) && requestedJobs > 0
? Math.max(1, Math.min(32, requestedJobs))
: null;
const version = packageVersion(sourceDir);
const artifacts = [];
const history = readBuildHistory(ctx.dataDir);
for (const [targetIndex, platform] of platforms.entries()) {
const target = rustTarget(platform);
const locked = settings.locked !== false;
let estimatedPackages = 0;
try {
estimatedPackages = await estimatePackageCount(sourceDir, target, locked);
} catch (error) {
ctx.build.output(`Package-count estimate unavailable: ${error?.message || error}`, "warn");
}
const progress = createTargetProgress(ctx, {
platform,
targetIndex,
targetCount: platforms.length,
estimatedPackages,
historicalDurationMs: Number(history[target]?.durationMs) || null,
});
progress.emit(true);
ctx.build.output(`\n=== Rust Lite ${platform} (${target}) ===`);
ctx.build.output(
`Release profile: opt-level=z, LTO, 1 codegen unit, stripped symbols, panic=abort`,
);
ctx.build.output(`Cargo jobs: ${jobs === null ? "automatic (host CPU count)" : jobs}`);
if (estimatedPackages > 0) ctx.build.output(`Estimated dependency graph: ${estimatedPackages} crates`);
const args = [
"cargo",
"build",
"--release",
"--target",
target,
"--message-format=json-render-diagnostics",
];
if (jobs !== null) args.push("--jobs", String(jobs));
if (locked) args.push("--locked");
const env = {
...process.env,
CARGO_TARGET_DIR: targetDir,
OVERLORD_LITE_DEFAULT_SERVER: String(payload.serverUrl || ""),
OVERLORD_LITE_DEFAULT_AGENT_TOKEN: String(payload.agentToken || ""),
OVERLORD_LITE_DEFAULT_BUILD_TAG: String(payload.buildTag || ""),
OVERLORD_LITE_DEFAULT_TLS_SPKI_PINS: tlsSpkiPins.join(","),
};
const proc = Bun.spawn(args, {
cwd: sourceDir,
env,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
});
const progressTimer = setInterval(() => progress.emit(), 2_000);
let exitCode;
try {
await Promise.all([
streamPipe(proc.stdout, (line) => {
let message;
try {
message = JSON.parse(line);
} catch {
ctx.build.output(line, "info");
return;
}
if (message.reason === "compiler-artifact" && typeof message.package_id === "string") {
progress.noteArtifact(message.package_id, message.fresh === true);
} else if (message.reason === "compiler-message" && message.message?.rendered) {
const level = message.message.level === "error"
? "error"
: message.message.level === "warning"
? "warn"
: "info";
ctx.build.output(String(message.message.rendered).trimEnd(), level);
}
}),
streamPipe(proc.stderr, (line) => {
if (/^\s*Compiling\s+overlord-lite\b/i.test(line)) progress.markLinking();
ctx.build.output(line, /^\s*error(?:\[|:)/i.test(line) ? "error" : "info");
}),
]);
exitCode = await proc.exited;
} finally {
clearInterval(progressTimer);
}
if (exitCode !== 0) {
throw new Error(
`Cargo failed for ${platform} (exit ${exitCode}). Install the ${target} Rust target and its required cross-linker.`,
);
}
progress.markFinalizing();
const extension = TARGETS[platform].extension;
const builtPath = path.join(targetDir, target, "release", `overlord-lite${extension}`);
if (!fs.existsSync(builtPath)) throw new Error(`Cargo did not create ${builtPath}`);
const baseName = String(payload.outputName || "overlord-lite").replace(/[^A-Za-z0-9._-]/g, "") || "overlord-lite";
const filename = `${baseName}-${platform}${extension}`;
fs.copyFileSync(builtPath, path.join(outputDir, filename));
artifacts.push({
name: `Overlord Lite ${platform}`,
filename,
platform,
version,
});
const completed = progress.markFinished();
recordBuildDuration(ctx.dataDir, target, completed.elapsedMs);
const compiledPackages = Math.max(0, completed.completedPackages - completed.freshPackages);
ctx.build.output(
`Completed ${platform} in ${formatDuration(completed.elapsedMs)} ` +
`(${completed.freshPackages} cached, ${compiledPackages} compiled crates)`,
"success",
);
ctx.build.output(`Artifact ready: ${filename}`, "success");
}
return { artifacts };
},
rpc: {
async status(_ctx, _params) {
let cargo = null;
try {
cargo = await capture(["cargo", "--version"], process.cwd());
} catch (error) {
cargo = `Unavailable: ${error?.message || error}`;
}
return {
cargo,
host: `${process.platform}-${process.arch}`,
note: "Cross-target builds require the corresponding rustup target and linker.",
};
},
},
};