c3d836d45c34c66599c09ea95ff8fb147d9c1e009b06df9052262f6ccff1dbe31import log from "../../seed/log.js";
2import getTools, { setEnergyService } from "./tools.js";
3
4export async function init(core) {
5 if (core.energy) setEnergyService(core.energy);
6
7 log.warn("Shell", "Shell extension loaded (confined). AI has system access where explicitly allowed.");
8
9 return {
10 tools: getTools(),
11 };
12}
131export default {
2 name: "shell",
3 version: "1.0.1",
4 builtFor: "TreeOS",
5 scope: "confined",
6 description:
7 "Gives the AI direct shell access to the land server. The execute-shell tool runs " +
8 "any command through Node.js child_process with a 30 second timeout and 8KB output " +
9 "cap. Three security layers. Layer 1: confined scope. Shell is inactive everywhere by " +
10 "default. Operators explicitly allow it at specific nodes with ext-allow shell. A DevOps " +
11 "branch gets shell. The rest of the tree never sees it. Layer 2: admin-only. The handler " +
12 "checks user.isAdmin before every execution and rejects all non-admin callers. Layer 3: " +
13 "energy metering. Each execution costs energy to prevent rapid-fire abuse. A regex " +
14 "blocklist catches the most destructive patterns (rm -rf, fork bombs, disk writes, " +
15 "firewall flushes, pipe-to-shell curls, password changes, service shutdowns, command " +
16 "substitution) even for admins. The blocklist prevents common accidents. It is not a " +
17 "sandbox. Shell access is real shell access. Confine it to positions where that is " +
18 "acceptable. Every command is logged with the user ID.",
19
20 needs: {
21 models: ["User"],
22 },
23
24 optional: {
25 services: ["energy"],
26 },
27
28 provides: {
29 models: {},
30 routes: false,
31 tools: true,
32 jobs: false,
33 orchestrator: false,
34 energyActions: {
35 shellExecute: { cost: 5 },
36 },
37 sessionTypes: {},
38 cli: [],
39 },
40};
411import { z } from "zod";
2import { execFile } from "child_process";
3import { promisify } from "util";
4import User from "../../seed/models/user.js";
5import log from "../../seed/log.js";
6
7const execFileAsync = promisify(execFile);
8const TIMEOUT_MS = 30000;
9const MAX_OUTPUT = 8000;
10
11let _useEnergy = async () => ({ energyUsed: 0 });
12
13export function setEnergyService(energy) {
14 if (energy?.useEnergy) _useEnergy = energy.useEnergy;
15}
16
17export default function getTools() {
18 return [
19 {
20 name: "execute-shell",
21 description: "Execute a shell command on the land server. Returns stdout and stderr. God-tier users only. 30 second timeout.",
22 schema: {
23 command: z.string().describe("The shell command to execute."),
24 userId: z.string().describe("Injected by server. Ignore."),
25 },
26 annotations: {
27 readOnlyHint: false,
28 destructiveHint: true,
29 idempotentHint: false,
30 openWorldHint: true,
31 },
32 async handler({ command, userId }) {
33 const user = await User.findById(userId).select("isAdmin").lean();
34 if (!user || !user.isAdmin) {
35 return { content: [{ type: "text", text: "Permission denied. Shell access requires admin." }] };
36 }
37 if (!command) {
38 return { content: [{ type: "text", text: "No command provided." }] };
39 }
40
41 // Block dangerous commands
42 const BLOCKED = [
43 /\brm\s+.*-[a-zA-Z]*r/i, // rm -r, rm -rf, rm -fr
44 /\brm\s+.*\//, // rm with any path
45 /\brmdir\b/i, // rmdir
46 /\bmkfs\b/i, // format filesystem
47 /\bdd\s+/i, // disk destroy
48 /\b:\(\)\{.*\|.*\}/, // fork bomb
49 /\bchmod\s+(777|000|\+s)\b/, // dangerous permission changes
50 /\bchown\b/i, // any ownership change
51 />\s*\/dev\/sd/, // overwrite disk devices
52 /\bshutdown\b/i, // shutdown
53 /\breboot\b/i, // reboot
54 /\binit\s+[06]\b/, // init shutdown/reboot
55 /\bsystemctl\s+(stop|disable|mask|restart)\b/i, // stop/restart services
56 /\bkill\s+-9\s+1\b/, // kill init
57 /\biptables\s+-F\b/i, // flush firewall
58 /\bufw\s+(disable|reset)\b/i, // disable firewall
59 /\bpasswd\b/i, // change passwords
60 /\busermod\b/i, // modify users
61 /\buserdel\b/i, // delete users
62 /\bcurl\b.*\|\s*(bash|sh)\b/i, // pipe to shell
63 /\bwget\b.*\|\s*(bash|sh)\b/i, // pipe to shell
64 /\beval\b/i, // eval execution
65 /`[^`]*`/, // backtick command substitution
66 /\$\([^)]*\)/, // $() command substitution
67 ];
68
69 const blocked = BLOCKED.find(re => re.test(command));
70 if (blocked) {
71 log.warn("Shell", `BLOCKED dangerous command from ${userId}: ${command.slice(0, 200)}`);
72 return { content: [{ type: "text", text: "Blocked: this command pattern is not allowed for safety. Use the server directly for destructive operations." }] };
73 }
74
75 // Energy metering
76 try {
77 await _useEnergy({ userId, action: "shellExecute" });
78 } catch {
79 return { content: [{ type: "text", text: "Insufficient energy for shell execution." }] };
80 }
81
82 log.warn("Shell", `${userId} executing: ${command.slice(0, 200)}`);
83
84 // Execute via /bin/sh -c but with execFile (no double shell interpretation).
85 // execFile with explicit shell path avoids the implicit shell spawning of exec().
86 // The command is passed as a single argument to sh -c, not interpreted by Node.
87 try {
88 const { stdout, stderr } = await execFileAsync("/bin/sh", ["-c", command], {
89 timeout: TIMEOUT_MS,
90 cwd: process.cwd(),
91 maxBuffer: 1024 * 1024,
92 env: { ...process.env, PATH: process.env.PATH },
93 });
94 const out = (stdout || "").slice(0, MAX_OUTPUT);
95 const err = (stderr || "").slice(0, MAX_OUTPUT);
96 let result = out || "(no output)";
97 if (err) result += "\n--- stderr ---\n" + err;
98 return { content: [{ type: "text", text: result }] };
99 } catch (err) {
100 const output = [err.stdout, err.stderr, err.message].filter(Boolean).join("\n").slice(0, MAX_OUTPUT);
101 return { content: [{ type: "text", text: `Command failed (exit ${err.code || "?"})\n${output}` }] };
102 }
103 },
104 },
105 ];
106}
107
| Version | Published | Downloads |
|---|---|---|
| 1.0.1 | 38d ago | 0 |
| 1.0.0 | 48d ago | 0 |
treeos ext star shell
Post comments from the CLI: treeos ext comment shell "your comment"
Max 3 comments per extension. One star and one flag per user.
Loading comments...