feat(settings): admin switch to allow or block members' test runs

Settings → AI tools → Tests on this server (admin, S3 only). Stored in
/opt/agenthub/app/member-policy.json, which member-guard.js reads as the
member at each Bash call — no restart; no file = blocked. GET/PUT
/api/ai-tools/member-policy, audited, 409 ENOS3 outside S3.
This commit is contained in:
2026-11-03 11:37:13 +07:00
parent e6ce8009b1
commit 497faae3bf
14 changed files with 309 additions and 5 deletions
+9 -3
View File
@@ -8,7 +8,8 @@
*
* - `PreToolUse` (matcher Bash): denies a test run or a container command
* (`isHeavy`) — the member commits, pulls the branch on their own machine
* and runs it there. Claude gets the reason and stops trying.
* and runs it there. Claude gets the reason and stops trying. An admin
* can allow them again (Settings → AI tools, server/lib/member-policy.js).
* - `UserPromptSubmit`: a member runs at most `MAX_AI_SESSIONS` AI sessions
* at once (server/lib/ai-procs.js). The dashboard refuses a pane past the
* limit itself (ESESSIONLIMIT); this catches a `claude` typed into a
@@ -25,6 +26,7 @@ const os = require("node:os");
const path = require("node:path");
const procs = require(path.join(__dirname, "..", "server", "lib", "ai-procs.js"));
const policy = require(path.join(__dirname, "..", "server", "lib", "member-policy.js"));
/** Test runners and container tools, by the program a command segment runs. */
const HEAVY_BINS = new Set([
@@ -139,8 +141,12 @@ const DENY_REASON =
"run them. Finish the change, tell the user which tests to run, and let them " +
"commit, pull the branch on their own machine and test there.";
function guardTool(input) {
if (input.tool_name !== "Bash" || !isHeavy(input.tool_input?.command)) return;
/** The admin's switch (Settings → AI tools), beside this script in the app root. */
const testsAllowed = () =>
policy.read(path.join(__dirname, "..", policy.FILE)).allowTests === true;
function guardTool(input, allowed = testsAllowed()) {
if (allowed || input.tool_name !== "Bash" || !isHeavy(input.tool_input?.command)) return;
process.stdout.write(
JSON.stringify({
hookSpecificOutput: {