build hook plugins

This commit is contained in:
KingKDot
2026-06-22 12:54:12 -05:00
parent 6180e7f1a2
commit 7aaf115e2f
20 changed files with 1857 additions and 81 deletions
+16
View File
@@ -0,0 +1,16 @@
# Sample Build Hooks Plugin
This is a server-side-only sample for the agent builder hook API.
It demonstrates:
- `onBuildPrepare` and `onBuildTarget` for observing build state.
- `buildHooks.post_build` for seeing the raw Go build output.
- `buildHooks.before_donut` and `buildHooks.after_donut` for Donut shellcode conversion.
- `buildHooks.before_sgn` and `buildHooks.after_sgn` for SGN shellcode encoding.
- `onBuildArtifact` for replacing the downloadable output with a `.txt` file containing `test`.
- A custom Build Plugins button named `Build Test TXT`.
- `onBuildComplete` and `onBuildFailed` for post-build automation.
- A small UI that reads recent hook activity from the plugin's SQLite database.
Run `build.bat` from this directory to create `sample-build-hooks.zip`, then upload it on the Plugins page or place the zip where the server loads plugins.
+26
View File
@@ -0,0 +1,26 @@
@echo off
setlocal enabledelayedexpansion
set "PLUGIN_DIR=%~dp0"
set "PLUGIN_NAME=sample-build-hooks"
set "ZIP_OUT=%PLUGIN_DIR%%PLUGIN_NAME%.zip"
if exist "%ZIP_OUT%" del /f /q "%ZIP_OUT%"
pushd "%PLUGIN_DIR%"
powershell -NoProfile -ExecutionPolicy Bypass -Command "$zip = '%ZIP_OUT%'; $items = @('config.json', '%PLUGIN_NAME%.html', '%PLUGIN_NAME%.css', '%PLUGIN_NAME%.js', 'server.js'); Compress-Archive -Path $items -DestinationPath $zip -Force"
set "ZIP_STATUS=%ERRORLEVEL%"
popd
if not "%ZIP_STATUS%"=="0" exit /b %ZIP_STATUS%
if defined PLUGIN_SIGN_KEY (
where bun >nul 2>&1
if not errorlevel 1 (
echo [sign] Signing plugin with key: %PLUGIN_SIGN_KEY%
bun run "%~dp0..\..\Overlord-Server\scripts\plugin-sign.ts" --key "%PLUGIN_SIGN_KEY%" "%ZIP_OUT%"
) else (
echo [warn] bun not found, skipping plugin signing
)
)
echo [ok] %ZIP_OUT%
@@ -0,0 +1,41 @@
.sample-build-hooks {
display: grid;
gap: 12px;
}
.sample-build-hooks__header {
align-items: center;
display: flex;
gap: 12px;
justify-content: space-between;
}
.sample-build-hooks h2 {
color: #e5e7eb;
font-size: 18px;
font-weight: 700;
margin: 0;
}
.sample-build-hooks button {
background: #0f766e;
border: 1px solid #14b8a6;
border-radius: 8px;
color: #ecfeff;
cursor: pointer;
font-size: 13px;
padding: 8px 12px;
}
.sample-build-hooks pre {
background: rgba(15, 23, 42, 0.7);
border: 1px solid #334155;
border-radius: 8px;
color: #cbd5e1;
font-size: 12px;
margin: 0;
min-height: 180px;
overflow: auto;
padding: 12px;
white-space: pre-wrap;
}
@@ -0,0 +1,7 @@
<div class="sample-build-hooks">
<div class="sample-build-hooks__header">
<h2>Sample Build Hooks</h2>
<button id="sample-build-hooks-refresh" type="button">Refresh</button>
</div>
<pre id="sample-build-hooks-output">Loading...</pre>
</div>
@@ -0,0 +1,26 @@
const output = document.getElementById("sample-build-hooks-output");
const refreshButton = document.getElementById("sample-build-hooks-refresh");
async function callRpc(method, params) {
const res = await fetch("/api/plugins/sample-build-hooks/rpc", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ method, params }),
});
const data = await res.json();
if (!data.ok) throw new Error(data.error || "RPC failed");
return data.result;
}
async function refresh() {
output.textContent = "Loading...";
try {
const result = await callRpc("recent");
output.textContent = JSON.stringify(result, null, 2);
} catch (err) {
output.textContent = `Error: ${err.message || err}`;
}
}
refreshButton?.addEventListener("click", refresh);
refresh();
+189
View File
@@ -0,0 +1,189 @@
import fs from "fs";
import path from "path";
function record(ctx, hook, data = {}) {
ctx.db
.prepare("INSERT INTO build_hook_events(hook, data, created_at) VALUES (?, ?, ?)")
.run(hook, JSON.stringify(data), Date.now());
}
function safeJson(value) {
try {
return JSON.stringify(value, null, 2);
} catch {
return String(value);
}
}
function buildSettings(ctx, payload) {
const record = payload.config?.buildPlugins?.[ctx.pluginId];
return {
enabled: record?.enabled !== false,
replaceOutput: record?.settings?.replaceOutput !== false,
note: String(record?.settings?.note || "test"),
};
}
export default {
setup(ctx) {
ctx.db.exec(`
CREATE TABLE IF NOT EXISTS build_hook_events (
id INTEGER PRIMARY KEY AUTOINCREMENT,
hook TEXT NOT NULL,
data TEXT NOT NULL,
created_at INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS build_hook_events_recent ON build_hook_events(created_at DESC);
`);
ctx.log.info("sample build hook plugin ready");
},
onBuildPrepare(ctx, payload) {
const settings = buildSettings(ctx, payload);
if (!settings.enabled) return null;
record(ctx, "prepare", {
buildId: payload.buildId,
platforms: payload.config?.platforms || [],
settings,
});
return {
message: "prepare hook observed build config",
};
},
onBuildTarget(ctx, payload) {
const settings = buildSettings(ctx, payload);
if (!settings.enabled) return null;
record(ctx, "target", {
buildId: payload.buildId,
platform: payload.platform,
outputName: payload.outputName,
settings,
});
return { message: `target hook observed ${payload.platform}` };
},
buildHooks: {
post_build(ctx, payload) {
if (!buildSettings(ctx, payload).enabled) return null;
record(ctx, "post_build", {
buildId: payload.buildId,
platform: payload.platform,
filename: payload.file?.filename,
size: payload.file?.size,
});
},
before_donut(ctx, payload) {
if (!buildSettings(ctx, payload).enabled) return null;
record(ctx, "before_donut", {
buildId: payload.buildId,
platform: payload.platform,
input: payload.file?.filename,
output: payload.outputFilename,
donutArch: payload.donutArch,
});
return { message: `before Donut: ${payload.file?.filename}` };
},
after_donut(ctx, payload) {
if (!buildSettings(ctx, payload).enabled) return null;
record(ctx, "after_donut", {
buildId: payload.buildId,
platform: payload.platform,
filename: payload.file?.filename,
size: payload.file?.size,
donutArch: payload.donutArch,
});
return { message: `after Donut: ${payload.file?.filename}` };
},
before_sgn(ctx, payload) {
if (!buildSettings(ctx, payload).enabled) return null;
record(ctx, "before_sgn", {
buildId: payload.buildId,
platform: payload.platform,
input: payload.file?.filename,
output: payload.outputFilename,
iterations: payload.iterations,
});
return { message: `before SGN: ${payload.file?.filename}` };
},
after_sgn(ctx, payload) {
if (!buildSettings(ctx, payload).enabled) return null;
record(ctx, "after_sgn", {
buildId: payload.buildId,
platform: payload.platform,
filename: payload.file?.filename,
size: payload.file?.size,
iterations: payload.iterations,
});
return { message: `after SGN: ${payload.file?.filename}` };
},
},
onBuildArtifact(ctx, payload) {
const settings = buildSettings(ctx, payload);
if (!settings.enabled) return null;
record(ctx, "artifact", {
buildId: payload.buildId,
platform: payload.platform,
filename: payload.file?.filename,
size: payload.file?.size,
settings,
});
if (!settings.replaceOutput) {
return { message: "test TXT replacement disabled by plugin setting" };
}
const txtName = `${path.parse(payload.file.filename).name}.txt`;
const txtPath = path.join(payload.outDir, txtName);
fs.writeFileSync(txtPath, `${settings.note}\n`, "utf8");
return {
file: { filename: txtName },
message: `replaced build output with ${txtName}`,
};
},
onBuildComplete(ctx, payload) {
if (!buildSettings(ctx, payload).enabled) return null;
record(ctx, "complete", {
buildId: payload.buildId,
files: payload.files?.map((file) => file.filename) || [],
});
ctx.broadcast("build_complete", { buildId: payload.buildId, files: payload.files?.length || 0 });
},
onBuildFailed(ctx, payload) {
if (!buildSettings(ctx, payload).enabled) return null;
record(ctx, "failed", {
buildId: payload.buildId,
error: payload.error,
});
ctx.broadcast("build_failed", { buildId: payload.buildId, error: payload.error });
},
rpc: {
recent(ctx) {
const rows = ctx.db
.prepare("SELECT hook, data, created_at FROM build_hook_events ORDER BY created_at DESC LIMIT 25")
.all();
return rows.map((row) => ({
hook: row.hook,
createdAt: new Date(row.created_at).toISOString(),
data: JSON.parse(row.data),
}));
},
clear(ctx, _params, { caller }) {
if (caller.role !== "admin") throw new Error("Admin only");
ctx.db.exec("DELETE FROM build_hook_events");
ctx.broadcast("cleared", { by: caller.id });
return { ok: true };
},
},
};