@schuttdev/gigai 0.3.5 → 0.4.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,50 +1,39 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ErrorCode,
4
- GigaiError
5
- } from "./chunk-P53UVHTF.js";
4
+ GigaiError,
5
+ canExecuteCommand,
6
+ canUseSudo,
7
+ expandTilde,
8
+ validateCommandArgs
9
+ } from "./chunk-ROMGFLOH.js";
6
10
 
7
- // ../server/dist/chunk-APX4HM32.mjs
11
+ // ../server/dist/chunk-BO7QE6T2.mjs
8
12
  import { spawn } from "child_process";
9
- var SHELL_INTERPRETERS = /* @__PURE__ */ new Set([
10
- "sh",
11
- "bash",
12
- "zsh",
13
- "fish",
14
- "csh",
15
- "tcsh",
16
- "dash",
17
- "ksh",
18
- "env",
19
- "xargs",
20
- "nohup",
21
- "strace",
22
- "ltrace"
23
- ]);
24
13
  var MAX_OUTPUT_SIZE = 10 * 1024 * 1024;
25
- async function execCommandSafe(command, args, config) {
26
- if (!config.allowlist.includes(command)) {
27
- throw new GigaiError(
28
- ErrorCode.COMMAND_NOT_ALLOWED,
29
- `Command not in allowlist: ${command}. Allowed: ${config.allowlist.join(", ")}`
30
- );
14
+ async function execCommandSafe(command, args, config, tier = "strict") {
15
+ if (command === "sudo") {
16
+ const sudoCheck = canUseSudo(config.allowSudo);
17
+ if (!sudoCheck.allowed) {
18
+ throw new GigaiError(ErrorCode.COMMAND_NOT_ALLOWED, sudoCheck.reason);
19
+ }
31
20
  }
32
- if (command === "sudo" && !config.allowSudo) {
33
- throw new GigaiError(ErrorCode.COMMAND_NOT_ALLOWED, "sudo is not allowed");
21
+ const check = canExecuteCommand(tier, command, config.allowlist);
22
+ if (!check.allowed) {
23
+ throw new GigaiError(ErrorCode.COMMAND_NOT_ALLOWED, check.reason);
34
24
  }
35
- if (SHELL_INTERPRETERS.has(command)) {
36
- throw new GigaiError(
37
- ErrorCode.COMMAND_NOT_ALLOWED,
38
- `Shell interpreter not allowed: ${command}`
39
- );
25
+ const argCheck = validateCommandArgs(tier, command, args);
26
+ if (!argCheck.allowed) {
27
+ throw new GigaiError(ErrorCode.COMMAND_NOT_ALLOWED, argCheck.reason);
40
28
  }
41
29
  for (const arg of args) {
42
30
  if (arg.includes("\0")) {
43
31
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Null byte in argument");
44
32
  }
45
33
  }
34
+ const expandedArgs = args.map(expandTilde);
46
35
  return new Promise((resolve, reject) => {
47
- const child = spawn(command, ["--", ...args], {
36
+ const child = spawn(command, expandedArgs, {
48
37
  shell: false,
49
38
  stdio: ["ignore", "pipe", "pipe"]
50
39
  });
@@ -1,5 +1,192 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // ../server/dist/chunk-NO4R43QM.mjs
4
+ import { resolve } from "path";
5
+ import { homedir } from "os";
6
+ var DEFAULT_SECURITY = { default: "strict", overrides: {} };
7
+ function getEffectiveTier(config, toolName) {
8
+ const c = config ?? DEFAULT_SECURITY;
9
+ return c.overrides[toolName] ?? c.default;
10
+ }
11
+ var STANDARD_DENYLIST = /* @__PURE__ */ new Set([
12
+ "dd",
13
+ // raw disk I/O
14
+ "mkfs",
15
+ // format filesystem
16
+ "fdisk",
17
+ // partition editor
18
+ "parted",
19
+ // partition editor
20
+ "mkswap",
21
+ // overwrite partition with swap
22
+ "mount",
23
+ // arbitrary fs mounting
24
+ "umount",
25
+ // unmount filesystems
26
+ "insmod",
27
+ // load kernel modules
28
+ "rmmod",
29
+ // remove kernel modules
30
+ "modprobe",
31
+ // kernel module loader
32
+ "iptables",
33
+ // firewall rules
34
+ "ip6tables",
35
+ // ipv6 firewall
36
+ "reboot",
37
+ // reboot machine
38
+ "shutdown",
39
+ // power off
40
+ "halt",
41
+ // halt machine
42
+ "poweroff",
43
+ // power off
44
+ "init",
45
+ // change runlevel
46
+ "telinit",
47
+ // change runlevel
48
+ "systemctl",
49
+ // service management (can make remote machine unreachable)
50
+ "chown"
51
+ // change file ownership
52
+ ]);
53
+ var SHELL_INTERPRETERS = /* @__PURE__ */ new Set([
54
+ "sh",
55
+ "bash",
56
+ "zsh",
57
+ "fish",
58
+ "csh",
59
+ "tcsh",
60
+ "dash",
61
+ "ksh",
62
+ "env",
63
+ "xargs",
64
+ "nohup",
65
+ "strace",
66
+ "ltrace"
67
+ ]);
68
+ function canExecuteCommand(tier, command, allowlist) {
69
+ if (command.includes("\0")) {
70
+ return { allowed: false, reason: "Null byte in command" };
71
+ }
72
+ switch (tier) {
73
+ case "strict": {
74
+ const list = allowlist ?? [];
75
+ if (list.length > 0 && !list.includes(command)) {
76
+ return { allowed: false, reason: `Command not in allowlist: ${command}. Allowed: ${list.join(", ")}` };
77
+ }
78
+ if (SHELL_INTERPRETERS.has(command)) {
79
+ return { allowed: false, reason: `Shell interpreter not allowed: ${command}` };
80
+ }
81
+ return { allowed: true };
82
+ }
83
+ case "standard": {
84
+ if (STANDARD_DENYLIST.has(command)) {
85
+ return { allowed: false, reason: `Command blocked by security policy: ${command}` };
86
+ }
87
+ if (SHELL_INTERPRETERS.has(command)) {
88
+ return { allowed: false, reason: `Shell interpreter not allowed: ${command}` };
89
+ }
90
+ return { allowed: true };
91
+ }
92
+ case "unrestricted":
93
+ return { allowed: true };
94
+ }
95
+ }
96
+ var PROTECTED_PATHS = /* @__PURE__ */ new Set([
97
+ "/",
98
+ "/bin",
99
+ "/boot",
100
+ "/dev",
101
+ "/etc",
102
+ "/home",
103
+ "/lib",
104
+ "/opt",
105
+ "/proc",
106
+ "/root",
107
+ "/sbin",
108
+ "/sys",
109
+ "/usr",
110
+ "/var",
111
+ "/Users",
112
+ "/System",
113
+ "/Library",
114
+ "/Applications"
115
+ ]);
116
+ function hasRecursiveFlag(args) {
117
+ for (const a of args) {
118
+ if (a === "--") break;
119
+ if (a === "-r" || a === "-R" || a === "--recursive") return true;
120
+ if (a.startsWith("-") && !a.startsWith("--") && /[rR]/.test(a)) return true;
121
+ }
122
+ return false;
123
+ }
124
+ function validateCommandArgs(tier, command, args) {
125
+ if (tier === "unrestricted") return { allowed: true };
126
+ if (command === "rm" && hasRecursiveFlag(args)) {
127
+ const home = homedir();
128
+ for (const arg of args) {
129
+ if (arg.startsWith("-")) continue;
130
+ const resolved = resolve(arg);
131
+ if (PROTECTED_PATHS.has(resolved) || resolved === home) {
132
+ return { allowed: false, reason: `Refusing to recursively remove critical path: ${resolved}` };
133
+ }
134
+ }
135
+ }
136
+ return { allowed: true };
137
+ }
138
+ function canUseSudo(allowSudo) {
139
+ if (allowSudo === false) return { allowed: false, reason: "sudo is not allowed" };
140
+ return { allowed: true };
141
+ }
142
+ var STANDARD_BLOCKED_PATHS = [
143
+ ".ssh",
144
+ ".gnupg",
145
+ ".gpg",
146
+ ".config/gigai",
147
+ ".aws",
148
+ ".azure",
149
+ ".gcloud",
150
+ ".kube",
151
+ ".docker"
152
+ ];
153
+ function canAccessPath(tier, resolvedPath, allowedPaths) {
154
+ switch (tier) {
155
+ case "strict": {
156
+ const paths = allowedPaths ?? [];
157
+ if (paths.length === 0) {
158
+ return { allowed: true };
159
+ }
160
+ const ok = paths.some((allowed) => {
161
+ const r = resolve(allowed);
162
+ return resolvedPath === r || resolvedPath.startsWith(r.endsWith("/") ? r : r + "/");
163
+ });
164
+ if (!ok) return { allowed: false, reason: `Path not within allowed directories: ${resolvedPath}` };
165
+ return { allowed: true };
166
+ }
167
+ case "standard": {
168
+ const home = homedir();
169
+ for (const blocked of STANDARD_BLOCKED_PATHS) {
170
+ const full = resolve(home, blocked);
171
+ if (resolvedPath === full || resolvedPath.startsWith(full + "/")) {
172
+ return { allowed: false, reason: `Path blocked by security policy: ${blocked}` };
173
+ }
174
+ }
175
+ return { allowed: true };
176
+ }
177
+ case "unrestricted":
178
+ return { allowed: true };
179
+ }
180
+ }
181
+ function expandTilde(p) {
182
+ if (p === "~") return homedir();
183
+ if (p.startsWith("~/")) return homedir() + p.slice(1);
184
+ return p;
185
+ }
186
+ function shouldInjectSeparator(tier) {
187
+ return tier === "strict";
188
+ }
189
+
3
190
  // ../shared/dist/index.mjs
4
191
  import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
5
192
  import { z } from "zod";
@@ -252,14 +439,27 @@ var ServerConfigSchema = z.object({
252
439
  host: z.string().default("0.0.0.0"),
253
440
  https: HttpsConfigSchema.optional()
254
441
  });
442
+ var SecurityTierSchema = z.enum(["strict", "standard", "unrestricted"]);
443
+ var SecurityConfigSchema = z.object({
444
+ default: SecurityTierSchema.default("strict"),
445
+ overrides: z.record(SecurityTierSchema).default({})
446
+ });
255
447
  var GigaiConfigSchema = z.object({
256
448
  serverName: z.string().optional(),
257
449
  server: ServerConfigSchema,
258
450
  auth: AuthConfigSchema,
259
- tools: z.array(ToolConfigSchema).default([])
451
+ tools: z.array(ToolConfigSchema).default([]),
452
+ security: SecurityConfigSchema.optional()
260
453
  });
261
454
 
262
455
  export {
456
+ getEffectiveTier,
457
+ canExecuteCommand,
458
+ validateCommandArgs,
459
+ canUseSudo,
460
+ canAccessPath,
461
+ expandTilde,
462
+ shouldInjectSeparator,
263
463
  encrypt,
264
464
  decrypt,
265
465
  generateEncryptionKey,
@@ -1,10 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ErrorCode,
4
- GigaiError
5
- } from "./chunk-P53UVHTF.js";
4
+ GigaiError,
5
+ canAccessPath,
6
+ expandTilde
7
+ } from "./chunk-ROMGFLOH.js";
6
8
 
7
- // ../server/dist/chunk-OZTLH66B.mjs
9
+ // ../server/dist/chunk-HXHM5BKW.mjs
8
10
  import {
9
11
  readFile as fsReadFile,
10
12
  writeFile as fsWriteFile,
@@ -15,41 +17,34 @@ import { realpath } from "fs/promises";
15
17
  import { spawn } from "child_process";
16
18
  var MAX_OUTPUT_SIZE = 10 * 1024 * 1024;
17
19
  var MAX_READ_SIZE = 2 * 1024 * 1024;
18
- async function validatePath(targetPath, allowedPaths) {
19
- const resolved = resolve(targetPath);
20
+ async function validatePath(targetPath, allowedPaths, tier = "strict") {
21
+ const resolved = resolve(expandTilde(targetPath));
20
22
  let real;
21
23
  try {
22
24
  real = await realpath(resolved);
23
25
  } catch {
24
26
  real = resolved;
25
27
  }
26
- const isAllowed = allowedPaths.some((allowed) => {
27
- const resolvedAllowed = resolve(allowed);
28
- const allowedPrefix = resolvedAllowed.endsWith("/") ? resolvedAllowed : resolvedAllowed + "/";
29
- return real === resolvedAllowed || real.startsWith(allowedPrefix);
30
- });
31
- if (!isAllowed) {
32
- throw new GigaiError(
33
- ErrorCode.PATH_NOT_ALLOWED,
34
- `Path not within allowed directories: ${targetPath}`
35
- );
28
+ const check = canAccessPath(tier, real, allowedPaths);
29
+ if (!check.allowed) {
30
+ throw new GigaiError(ErrorCode.PATH_NOT_ALLOWED, check.reason);
36
31
  }
37
32
  return real;
38
33
  }
39
- async function readFileSafe(path, allowedPaths) {
40
- const safePath = await validatePath(path, allowedPaths);
34
+ async function readFileSafe(path, allowedPaths, tier = "strict") {
35
+ const safePath = await validatePath(path, allowedPaths, tier);
41
36
  return fsReadFile(safePath, "utf8");
42
37
  }
43
- async function listDirSafe(path, allowedPaths) {
44
- const safePath = await validatePath(path, allowedPaths);
38
+ async function listDirSafe(path, allowedPaths, tier = "strict") {
39
+ const safePath = await validatePath(path, allowedPaths, tier);
45
40
  const entries = await readdir(safePath, { withFileTypes: true });
46
41
  return entries.map((e) => ({
47
42
  name: e.name,
48
43
  type: e.isDirectory() ? "directory" : "file"
49
44
  }));
50
45
  }
51
- async function searchFilesSafe(path, pattern, allowedPaths) {
52
- const safePath = await validatePath(path, allowedPaths);
46
+ async function searchFilesSafe(path, pattern, allowedPaths, tier = "strict") {
47
+ const safePath = await validatePath(path, allowedPaths, tier);
53
48
  const results = [];
54
49
  let regex;
55
50
  try {
@@ -72,12 +67,12 @@ async function searchFilesSafe(path, pattern, allowedPaths) {
72
67
  await walk(safePath);
73
68
  return results;
74
69
  }
75
- async function readBuiltin(args, allowedPaths) {
70
+ async function readBuiltin(args, allowedPaths, tier = "strict") {
76
71
  const filePath = args[0];
77
72
  if (!filePath) {
78
73
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: read <file> [offset] [limit]");
79
74
  }
80
- const safePath = await validatePath(filePath, allowedPaths);
75
+ const safePath = await validatePath(filePath, allowedPaths, tier);
81
76
  const content = await fsReadFile(safePath, "utf8");
82
77
  if (content.length > MAX_READ_SIZE) {
83
78
  throw new GigaiError(
@@ -96,20 +91,20 @@ async function readBuiltin(args, allowedPaths) {
96
91
  }
97
92
  return { stdout: content, stderr: "", exitCode: 0 };
98
93
  }
99
- async function writeBuiltin(args, allowedPaths) {
94
+ async function writeBuiltin(args, allowedPaths, tier = "strict") {
100
95
  const filePath = args[0];
101
96
  const content = args[1];
102
97
  if (!filePath || content === void 0) {
103
98
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: write <file> <content>");
104
99
  }
105
- const safePath = await validatePath(filePath, allowedPaths);
100
+ const safePath = await validatePath(filePath, allowedPaths, tier);
106
101
  const { mkdir } = await import("fs/promises");
107
102
  const { dirname } = await import("path");
108
103
  await mkdir(dirname(safePath), { recursive: true });
109
104
  await fsWriteFile(safePath, content, "utf8");
110
105
  return { stdout: `Written: ${safePath}`, stderr: "", exitCode: 0 };
111
106
  }
112
- async function editBuiltin(args, allowedPaths) {
107
+ async function editBuiltin(args, allowedPaths, tier = "strict") {
113
108
  const filePath = args[0];
114
109
  const oldStr = args[1];
115
110
  const newStr = args[2];
@@ -117,7 +112,7 @@ async function editBuiltin(args, allowedPaths) {
117
112
  if (!filePath || oldStr === void 0 || newStr === void 0) {
118
113
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: edit <file> <old_string> <new_string> [--all]");
119
114
  }
120
- const safePath = await validatePath(filePath, allowedPaths);
115
+ const safePath = await validatePath(filePath, allowedPaths, tier);
121
116
  const content = await fsReadFile(safePath, "utf8");
122
117
  if (!content.includes(oldStr)) {
123
118
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "old_string not found in file");
@@ -137,13 +132,13 @@ async function editBuiltin(args, allowedPaths) {
137
132
  const count = replaceAll ? content.split(oldStr).length - 1 : 1;
138
133
  return { stdout: `Replaced ${count} occurrence(s) in ${safePath}`, stderr: "", exitCode: 0 };
139
134
  }
140
- async function globBuiltin(args, allowedPaths) {
135
+ async function globBuiltin(args, allowedPaths, tier = "strict") {
141
136
  const pattern = args[0];
142
137
  if (!pattern) {
143
138
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: glob <pattern> [path]");
144
139
  }
145
140
  const searchPath = args[1] ?? ".";
146
- const safePath = await validatePath(searchPath, allowedPaths);
141
+ const safePath = await validatePath(searchPath, allowedPaths, tier);
147
142
  const results = [];
148
143
  const globRegex = globToRegex(pattern);
149
144
  async function walk(dir) {
@@ -168,7 +163,7 @@ async function globBuiltin(args, allowedPaths) {
168
163
  await walk(safePath);
169
164
  return { stdout: results.join("\n"), stderr: "", exitCode: 0 };
170
165
  }
171
- async function grepBuiltin(args, allowedPaths) {
166
+ async function grepBuiltin(args, allowedPaths, tier = "strict") {
172
167
  if (args.length === 0) {
173
168
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: grep <pattern> [path] [--glob <filter>] [-i] [-n] [-C <num>]");
174
169
  }
@@ -199,7 +194,7 @@ async function grepBuiltin(args, allowedPaths) {
199
194
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "No search pattern provided");
200
195
  }
201
196
  const searchPath = positional[1] ?? ".";
202
- const safePath = await validatePath(searchPath, allowedPaths);
197
+ const safePath = await validatePath(searchPath, allowedPaths, tier);
203
198
  try {
204
199
  return await spawnGrep("rg", [pattern, safePath, "-n", ...flags]);
205
200
  } catch {
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  execCommandSafe
4
- } from "./chunk-GIUPSQGA.js";
4
+ } from "./chunk-NFKBLW3D.js";
5
5
  import {
6
6
  editBuiltin,
7
7
  globBuiltin,
@@ -11,15 +11,17 @@ import {
11
11
  readFileSafe,
12
12
  searchFilesSafe,
13
13
  writeBuiltin
14
- } from "./chunk-75GPR4GR.js";
14
+ } from "./chunk-VCFG6XXN.js";
15
15
  import {
16
16
  ErrorCode,
17
17
  GigaiConfigSchema,
18
18
  GigaiError,
19
19
  decrypt,
20
20
  encrypt,
21
- generateEncryptionKey
22
- } from "./chunk-P53UVHTF.js";
21
+ generateEncryptionKey,
22
+ getEffectiveTier,
23
+ shouldInjectSeparator
24
+ } from "./chunk-ROMGFLOH.js";
23
25
 
24
26
  // ../server/dist/index.mjs
25
27
  import { parseArgs } from "util";
@@ -46,7 +48,7 @@ import { nanoid as nanoid3 } from "nanoid";
46
48
  import { dirname as dirname2 } from "path";
47
49
  import { readFileSync } from "fs";
48
50
  import { resolve as resolve2 } from "path";
49
- import { platform, hostname as hostname2 } from "os";
51
+ import { platform, hostname as hostname2, userInfo } from "os";
50
52
  import { platform as platform2 } from "os";
51
53
  import { writeFile as writeFile2, readFile as readFile2, unlink, mkdir } from "fs/promises";
52
54
  import { join } from "path";
@@ -357,7 +359,7 @@ function sanitizeArgs(args) {
357
359
  var DEFAULT_TIMEOUT = 3e4;
358
360
  var KILL_GRACE_PERIOD = 5e3;
359
361
  var MAX_OUTPUT_SIZE = 10 * 1024 * 1024;
360
- function executeTool(entry, args, timeout) {
362
+ function executeTool(entry, args, timeout, tier = "strict") {
361
363
  const sanitized = sanitizeArgs(args);
362
364
  const effectiveTimeout = timeout ?? DEFAULT_TIMEOUT;
363
365
  let command;
@@ -367,7 +369,11 @@ function executeTool(entry, args, timeout) {
367
369
  switch (entry.type) {
368
370
  case "cli":
369
371
  command = entry.config.command;
370
- spawnArgs = [...entry.config.args ?? [], "--", ...sanitized];
372
+ spawnArgs = [...entry.config.args ?? []];
373
+ if (shouldInjectSeparator(tier)) {
374
+ spawnArgs.push("--");
375
+ }
376
+ spawnArgs.push(...sanitized);
371
377
  cwd = entry.config.cwd;
372
378
  env = entry.config.env;
373
379
  break;
@@ -794,7 +800,7 @@ var cronPlugin = fp5(async (server, opts) => {
794
800
  const executor = async (tool, args) => {
795
801
  const entry = server.registry.get(tool);
796
802
  if (entry.type === "builtin") {
797
- const { execCommandSafe: execCommandSafe2 } = await import("./shell-Z3WUF2GW-TVEYB5OM.js");
803
+ const { execCommandSafe: execCommandSafe2 } = await import("./shell-IBJTGIEW-UEOHS5M2.js");
798
804
  const {
799
805
  readFileSafe: readFileSafe2,
800
806
  listDirSafe: listDirSafe2,
@@ -804,7 +810,7 @@ var cronPlugin = fp5(async (server, opts) => {
804
810
  editBuiltin: editBuiltin2,
805
811
  globBuiltin: globBuiltin2,
806
812
  grepBuiltin: grepBuiltin2
807
- } = await import("./filesystem-FWNLRLL6-LQG7PWAF.js");
813
+ } = await import("./filesystem-CVLQFP7T-VBJWEKEE.js");
808
814
  const builtinConfig = entry.config.config ?? {};
809
815
  switch (entry.config.builtin) {
810
816
  case "filesystem": {
@@ -885,7 +891,8 @@ async function healthRoutes(server) {
885
891
  version: startupVersion,
886
892
  uptime: Date.now() - startTime,
887
893
  platform: platform(),
888
- hostname: hostname2()
894
+ hostname: hostname2(),
895
+ homeDir: userInfo().homedir
889
896
  };
890
897
  });
891
898
  }
@@ -955,10 +962,11 @@ async function execRoutes(server) {
955
962
  }, async (request) => {
956
963
  const { tool, args, timeout } = request.body;
957
964
  const entry = server.registry.get(tool);
965
+ const tier = getEffectiveTier(server.securityConfig, tool);
958
966
  if (entry.type === "builtin") {
959
- return handleBuiltin(entry.config, args);
967
+ return handleBuiltin(entry.config, args, tier);
960
968
  }
961
- const result = await server.executor.execute(entry, args, timeout);
969
+ const result = await server.executor.execute(entry, args, timeout, tier);
962
970
  return result;
963
971
  });
964
972
  server.post("/exec/mcp", {
@@ -992,65 +1000,65 @@ async function execRoutes(server) {
992
1000
  };
993
1001
  });
994
1002
  }
995
- async function handleBuiltin(config, args) {
1003
+ async function handleBuiltin(config, args, tier) {
996
1004
  const builtinConfig = config.config ?? {};
997
1005
  switch (config.builtin) {
998
1006
  // Legacy combined filesystem tool
999
1007
  case "filesystem": {
1000
- const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1008
+ const allowedPaths = builtinConfig.allowedPaths;
1001
1009
  const subcommand = args[0];
1002
1010
  const target = args[1] ?? ".";
1003
1011
  switch (subcommand) {
1004
1012
  case "read":
1005
- return { stdout: await readFileSafe(target, allowedPaths), stderr: "", exitCode: 0, durationMs: 0 };
1013
+ return { stdout: await readFileSafe(target, allowedPaths ?? [], tier), stderr: "", exitCode: 0, durationMs: 0 };
1006
1014
  case "list":
1007
- return { stdout: JSON.stringify(await listDirSafe(target, allowedPaths), null, 2), stderr: "", exitCode: 0, durationMs: 0 };
1015
+ return { stdout: JSON.stringify(await listDirSafe(target, allowedPaths ?? [], tier), null, 2), stderr: "", exitCode: 0, durationMs: 0 };
1008
1016
  case "search":
1009
- return { stdout: JSON.stringify(await searchFilesSafe(target, args[2] ?? ".*", allowedPaths), null, 2), stderr: "", exitCode: 0, durationMs: 0 };
1017
+ return { stdout: JSON.stringify(await searchFilesSafe(target, args[2] ?? ".*", allowedPaths ?? [], tier), null, 2), stderr: "", exitCode: 0, durationMs: 0 };
1010
1018
  default:
1011
1019
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, `Unknown filesystem subcommand: ${subcommand}. Use: read, list, search`);
1012
1020
  }
1013
1021
  }
1014
1022
  // Legacy shell tool
1015
1023
  case "shell": {
1016
- const allowlist = builtinConfig.allowlist ?? [];
1017
- const allowSudo = builtinConfig.allowSudo ?? false;
1024
+ const allowlist = builtinConfig.allowlist;
1025
+ const allowSudo = builtinConfig.allowSudo;
1018
1026
  const command = args[0];
1019
1027
  if (!command) {
1020
1028
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "No command specified");
1021
1029
  }
1022
- const result = await execCommandSafe(command, args.slice(1), { allowlist, allowSudo });
1030
+ const result = await execCommandSafe(command, args.slice(1), { allowlist, allowSudo }, tier);
1023
1031
  return { ...result, durationMs: 0 };
1024
1032
  }
1025
1033
  // --- New builtins ---
1026
1034
  case "read": {
1027
- const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1028
- return { ...await readBuiltin(args, allowedPaths), durationMs: 0 };
1035
+ const allowedPaths = builtinConfig.allowedPaths;
1036
+ return { ...await readBuiltin(args, allowedPaths ?? [], tier), durationMs: 0 };
1029
1037
  }
1030
1038
  case "write": {
1031
- const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1032
- return { ...await writeBuiltin(args, allowedPaths), durationMs: 0 };
1039
+ const allowedPaths = builtinConfig.allowedPaths;
1040
+ return { ...await writeBuiltin(args, allowedPaths ?? [], tier), durationMs: 0 };
1033
1041
  }
1034
1042
  case "edit": {
1035
- const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1036
- return { ...await editBuiltin(args, allowedPaths), durationMs: 0 };
1043
+ const allowedPaths = builtinConfig.allowedPaths;
1044
+ return { ...await editBuiltin(args, allowedPaths ?? [], tier), durationMs: 0 };
1037
1045
  }
1038
1046
  case "glob": {
1039
- const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1040
- return { ...await globBuiltin(args, allowedPaths), durationMs: 0 };
1047
+ const allowedPaths = builtinConfig.allowedPaths;
1048
+ return { ...await globBuiltin(args, allowedPaths ?? [], tier), durationMs: 0 };
1041
1049
  }
1042
1050
  case "grep": {
1043
- const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1044
- return { ...await grepBuiltin(args, allowedPaths), durationMs: 0 };
1051
+ const allowedPaths = builtinConfig.allowedPaths;
1052
+ return { ...await grepBuiltin(args, allowedPaths ?? [], tier), durationMs: 0 };
1045
1053
  }
1046
1054
  case "bash": {
1047
- const allowlist = builtinConfig.allowlist ?? [];
1048
- const allowSudo = builtinConfig.allowSudo ?? false;
1055
+ const allowlist = builtinConfig.allowlist;
1056
+ const allowSudo = builtinConfig.allowSudo;
1049
1057
  const command = args[0];
1050
1058
  if (!command) {
1051
1059
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "No command specified");
1052
1060
  }
1053
- const result = await execCommandSafe(command, args.slice(1), { allowlist, allowSudo });
1061
+ const result = await execCommandSafe(command, args.slice(1), { allowlist, allowSudo }, tier);
1054
1062
  return { ...result, durationMs: 0 };
1055
1063
  }
1056
1064
  default:
@@ -1224,6 +1232,7 @@ async function createServer(opts) {
1224
1232
  await server.register(registryPlugin, { config });
1225
1233
  await server.register(executorPlugin);
1226
1234
  await server.register(mcpPlugin, { config });
1235
+ server.decorate("securityConfig", config.security);
1227
1236
  if (configPath) {
1228
1237
  await server.register(cronPlugin, { configPath });
1229
1238
  }
@@ -1486,6 +1495,15 @@ async function runInit() {
1486
1495
  default: "7443"
1487
1496
  });
1488
1497
  const port = parseInt(portStr, 10);
1498
+ const securityTier = await select({
1499
+ message: "Security tier:",
1500
+ choices: [
1501
+ { name: "Strict \u2014 allowlist-only, explicit path restrictions (most secure)", value: "strict" },
1502
+ { name: "Standard \u2014 denylist blocks catastrophic commands, home dir open (recommended)", value: "standard" },
1503
+ { name: "Unrestricted \u2014 no restrictions, development only", value: "unrestricted" }
1504
+ ],
1505
+ default: "standard"
1506
+ });
1489
1507
  const selectedBuiltins = await checkbox({
1490
1508
  message: "Built-in tools to enable:",
1491
1509
  choices: [
@@ -1495,35 +1513,59 @@ async function runInit() {
1495
1513
  });
1496
1514
  const tools = [];
1497
1515
  if (selectedBuiltins.includes("filesystem")) {
1498
- const pathsStr = await input({
1499
- message: "Allowed filesystem paths (comma-separated):",
1500
- default: process.env.HOME ?? "~"
1501
- });
1502
- const allowedPaths = pathsStr.split(",").map((p) => p.trim());
1503
- tools.push({
1504
- type: "builtin",
1505
- name: "fs",
1506
- builtin: "filesystem",
1507
- description: "Read, list, and search files",
1508
- config: { allowedPaths }
1516
+ const restrictPaths = await confirm({
1517
+ message: "Restrict filesystem to specific paths?",
1518
+ default: false
1509
1519
  });
1520
+ if (restrictPaths) {
1521
+ const pathsStr = await input({
1522
+ message: "Allowed paths (comma-separated):",
1523
+ default: process.env.HOME ?? "~"
1524
+ });
1525
+ const allowedPaths = pathsStr.split(",").map((p) => p.trim());
1526
+ tools.push({
1527
+ type: "builtin",
1528
+ name: "fs",
1529
+ builtin: "filesystem",
1530
+ description: "Read, list, and search files",
1531
+ config: { allowedPaths }
1532
+ });
1533
+ } else {
1534
+ tools.push({
1535
+ type: "builtin",
1536
+ name: "fs",
1537
+ builtin: "filesystem",
1538
+ description: "Read, list, and search files",
1539
+ config: {}
1540
+ });
1541
+ }
1510
1542
  }
1511
1543
  if (selectedBuiltins.includes("shell")) {
1512
- const allowlistStr = await input({
1513
- message: "Allowed shell commands (comma-separated):",
1514
- default: "ls,cat,head,tail,grep,find,wc,echo,date,whoami,pwd,git,npm,node"
1544
+ const restrictCommands = await confirm({
1545
+ message: "Restrict shell to a command allowlist?",
1546
+ default: false
1515
1547
  });
1516
- const allowlist = allowlistStr.split(",").map((c) => c.trim());
1517
- const allowSudo = await confirm({
1518
- message: "Allow sudo?",
1548
+ const shellConfig = {};
1549
+ if (restrictCommands) {
1550
+ const allowlistStr = await input({
1551
+ message: "Allowed commands (comma-separated):",
1552
+ default: "ls,cat,head,tail,grep,find,wc,echo,date,whoami,pwd,git,npm,node"
1553
+ });
1554
+ shellConfig.allowlist = allowlistStr.split(",").map((c) => c.trim());
1555
+ }
1556
+ const blockSudo = await confirm({
1557
+ message: "Block sudo?",
1519
1558
  default: false
1520
1559
  });
1560
+ if (blockSudo) {
1561
+ shellConfig.allowSudo = false;
1562
+ }
1521
1563
  tools.push({
1522
1564
  type: "builtin",
1523
1565
  name: "shell",
1524
1566
  builtin: "shell",
1525
- description: "Execute allowed shell commands",
1526
- config: { allowlist, allowSudo }
1567
+ description: "Execute shell commands",
1568
+ config: shellConfig
1527
1569
  });
1528
1570
  }
1529
1571
  const configFilePath = await detectClaudeDesktopConfig();
@@ -1603,7 +1645,8 @@ async function runInit() {
1603
1645
  pairingTtlSeconds: 300,
1604
1646
  sessionTtlSeconds: 14400
1605
1647
  },
1606
- tools
1648
+ tools,
1649
+ security: { default: securityTier, overrides: {} }
1607
1650
  };
1608
1651
  const configPath = resolve4("gigai.config.json");
1609
1652
  await writeFile3(configPath, JSON.stringify(config, null, 2) + "\n", { mode: 384 });
@@ -9,8 +9,8 @@ import {
9
9
  searchFilesSafe,
10
10
  validatePath,
11
11
  writeBuiltin
12
- } from "./chunk-75GPR4GR.js";
13
- import "./chunk-P53UVHTF.js";
12
+ } from "./chunk-VCFG6XXN.js";
13
+ import "./chunk-ROMGFLOH.js";
14
14
  export {
15
15
  editBuiltin,
16
16
  globBuiltin,
package/dist/index.js CHANGED
@@ -4,12 +4,12 @@
4
4
  import { defineCommand, runMain } from "citty";
5
5
 
6
6
  // src/version.ts
7
- var VERSION = "0.3.5";
7
+ var VERSION = "0.4.0-beta.1";
8
8
 
9
9
  // src/index.ts
10
10
  async function requireServer() {
11
11
  try {
12
- return await import("./dist-2BIIMXAI.js");
12
+ return await import("./dist-XQICZQ57.js");
13
13
  } catch {
14
14
  console.error("Server dependencies not installed.");
15
15
  console.error("Run: npm install -g @schuttdev/gigai");
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  execCommandSafe
4
- } from "./chunk-GIUPSQGA.js";
5
- import "./chunk-P53UVHTF.js";
4
+ } from "./chunk-NFKBLW3D.js";
5
+ import "./chunk-ROMGFLOH.js";
6
6
  export {
7
7
  execCommandSafe
8
8
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@schuttdev/gigai",
3
- "version": "0.3.5",
3
+ "version": "0.4.0-beta.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "bin": {