@schuttdev/gigai 0.3.5 → 0.4.0-beta.0

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,10 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  ErrorCode,
4
- GigaiError
5
- } from "./chunk-P53UVHTF.js";
4
+ GigaiError,
5
+ canAccessPath
6
+ } from "./chunk-XL27JFUS.js";
6
7
 
7
- // ../server/dist/chunk-OZTLH66B.mjs
8
+ // ../server/dist/chunk-XK4BHB3B.mjs
8
9
  import {
9
10
  readFile as fsReadFile,
10
11
  writeFile as fsWriteFile,
@@ -15,7 +16,7 @@ import { realpath } from "fs/promises";
15
16
  import { spawn } from "child_process";
16
17
  var MAX_OUTPUT_SIZE = 10 * 1024 * 1024;
17
18
  var MAX_READ_SIZE = 2 * 1024 * 1024;
18
- async function validatePath(targetPath, allowedPaths) {
19
+ async function validatePath(targetPath, allowedPaths, tier = "strict") {
19
20
  const resolved = resolve(targetPath);
20
21
  let real;
21
22
  try {
@@ -23,33 +24,26 @@ async function validatePath(targetPath, allowedPaths) {
23
24
  } catch {
24
25
  real = resolved;
25
26
  }
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
- );
27
+ const check = canAccessPath(tier, real, allowedPaths);
28
+ if (!check.allowed) {
29
+ throw new GigaiError(ErrorCode.PATH_NOT_ALLOWED, check.reason);
36
30
  }
37
31
  return real;
38
32
  }
39
- async function readFileSafe(path, allowedPaths) {
40
- const safePath = await validatePath(path, allowedPaths);
33
+ async function readFileSafe(path, allowedPaths, tier = "strict") {
34
+ const safePath = await validatePath(path, allowedPaths, tier);
41
35
  return fsReadFile(safePath, "utf8");
42
36
  }
43
- async function listDirSafe(path, allowedPaths) {
44
- const safePath = await validatePath(path, allowedPaths);
37
+ async function listDirSafe(path, allowedPaths, tier = "strict") {
38
+ const safePath = await validatePath(path, allowedPaths, tier);
45
39
  const entries = await readdir(safePath, { withFileTypes: true });
46
40
  return entries.map((e) => ({
47
41
  name: e.name,
48
42
  type: e.isDirectory() ? "directory" : "file"
49
43
  }));
50
44
  }
51
- async function searchFilesSafe(path, pattern, allowedPaths) {
52
- const safePath = await validatePath(path, allowedPaths);
45
+ async function searchFilesSafe(path, pattern, allowedPaths, tier = "strict") {
46
+ const safePath = await validatePath(path, allowedPaths, tier);
53
47
  const results = [];
54
48
  let regex;
55
49
  try {
@@ -72,12 +66,12 @@ async function searchFilesSafe(path, pattern, allowedPaths) {
72
66
  await walk(safePath);
73
67
  return results;
74
68
  }
75
- async function readBuiltin(args, allowedPaths) {
69
+ async function readBuiltin(args, allowedPaths, tier = "strict") {
76
70
  const filePath = args[0];
77
71
  if (!filePath) {
78
72
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: read <file> [offset] [limit]");
79
73
  }
80
- const safePath = await validatePath(filePath, allowedPaths);
74
+ const safePath = await validatePath(filePath, allowedPaths, tier);
81
75
  const content = await fsReadFile(safePath, "utf8");
82
76
  if (content.length > MAX_READ_SIZE) {
83
77
  throw new GigaiError(
@@ -96,20 +90,20 @@ async function readBuiltin(args, allowedPaths) {
96
90
  }
97
91
  return { stdout: content, stderr: "", exitCode: 0 };
98
92
  }
99
- async function writeBuiltin(args, allowedPaths) {
93
+ async function writeBuiltin(args, allowedPaths, tier = "strict") {
100
94
  const filePath = args[0];
101
95
  const content = args[1];
102
96
  if (!filePath || content === void 0) {
103
97
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: write <file> <content>");
104
98
  }
105
- const safePath = await validatePath(filePath, allowedPaths);
99
+ const safePath = await validatePath(filePath, allowedPaths, tier);
106
100
  const { mkdir } = await import("fs/promises");
107
101
  const { dirname } = await import("path");
108
102
  await mkdir(dirname(safePath), { recursive: true });
109
103
  await fsWriteFile(safePath, content, "utf8");
110
104
  return { stdout: `Written: ${safePath}`, stderr: "", exitCode: 0 };
111
105
  }
112
- async function editBuiltin(args, allowedPaths) {
106
+ async function editBuiltin(args, allowedPaths, tier = "strict") {
113
107
  const filePath = args[0];
114
108
  const oldStr = args[1];
115
109
  const newStr = args[2];
@@ -117,7 +111,7 @@ async function editBuiltin(args, allowedPaths) {
117
111
  if (!filePath || oldStr === void 0 || newStr === void 0) {
118
112
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: edit <file> <old_string> <new_string> [--all]");
119
113
  }
120
- const safePath = await validatePath(filePath, allowedPaths);
114
+ const safePath = await validatePath(filePath, allowedPaths, tier);
121
115
  const content = await fsReadFile(safePath, "utf8");
122
116
  if (!content.includes(oldStr)) {
123
117
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "old_string not found in file");
@@ -137,13 +131,13 @@ async function editBuiltin(args, allowedPaths) {
137
131
  const count = replaceAll ? content.split(oldStr).length - 1 : 1;
138
132
  return { stdout: `Replaced ${count} occurrence(s) in ${safePath}`, stderr: "", exitCode: 0 };
139
133
  }
140
- async function globBuiltin(args, allowedPaths) {
134
+ async function globBuiltin(args, allowedPaths, tier = "strict") {
141
135
  const pattern = args[0];
142
136
  if (!pattern) {
143
137
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: glob <pattern> [path]");
144
138
  }
145
139
  const searchPath = args[1] ?? ".";
146
- const safePath = await validatePath(searchPath, allowedPaths);
140
+ const safePath = await validatePath(searchPath, allowedPaths, tier);
147
141
  const results = [];
148
142
  const globRegex = globToRegex(pattern);
149
143
  async function walk(dir) {
@@ -168,7 +162,7 @@ async function globBuiltin(args, allowedPaths) {
168
162
  await walk(safePath);
169
163
  return { stdout: results.join("\n"), stderr: "", exitCode: 0 };
170
164
  }
171
- async function grepBuiltin(args, allowedPaths) {
165
+ async function grepBuiltin(args, allowedPaths, tier = "strict") {
172
166
  if (args.length === 0) {
173
167
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "Usage: grep <pattern> [path] [--glob <filter>] [-i] [-n] [-C <num>]");
174
168
  }
@@ -199,7 +193,7 @@ async function grepBuiltin(args, allowedPaths) {
199
193
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "No search pattern provided");
200
194
  }
201
195
  const searchPath = positional[1] ?? ".";
202
- const safePath = await validatePath(searchPath, allowedPaths);
196
+ const safePath = await validatePath(searchPath, allowedPaths, tier);
203
197
  try {
204
198
  return await spawnGrep("rg", [pattern, safePath, "-n", ...flags]);
205
199
  } catch {
@@ -1,42 +1,24 @@
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
+ } from "./chunk-XL27JFUS.js";
6
8
 
7
- // ../server/dist/chunk-APX4HM32.mjs
9
+ // ../server/dist/chunk-NYES5MEC.mjs
8
10
  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
11
  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
- );
31
- }
32
- if (command === "sudo" && !config.allowSudo) {
33
- throw new GigaiError(ErrorCode.COMMAND_NOT_ALLOWED, "sudo is not allowed");
12
+ async function execCommandSafe(command, args, config, tier = "strict") {
13
+ if (command === "sudo") {
14
+ const sudoCheck = canUseSudo(config.allowSudo);
15
+ if (!sudoCheck.allowed) {
16
+ throw new GigaiError(ErrorCode.COMMAND_NOT_ALLOWED, sudoCheck.reason);
17
+ }
34
18
  }
35
- if (SHELL_INTERPRETERS.has(command)) {
36
- throw new GigaiError(
37
- ErrorCode.COMMAND_NOT_ALLOWED,
38
- `Shell interpreter not allowed: ${command}`
39
- );
19
+ const check = canExecuteCommand(tier, command, config.allowlist);
20
+ if (!check.allowed) {
21
+ throw new GigaiError(ErrorCode.COMMAND_NOT_ALLOWED, check.reason);
40
22
  }
41
23
  for (const arg of args) {
42
24
  if (arg.includes("\0")) {
@@ -44,7 +26,7 @@ async function execCommandSafe(command, args, config) {
44
26
  }
45
27
  }
46
28
  return new Promise((resolve, reject) => {
47
- const child = spawn(command, ["--", ...args], {
29
+ const child = spawn(command, args, {
48
30
  shell: false,
49
31
  stdio: ["ignore", "pipe", "pipe"]
50
32
  });
@@ -1,5 +1,145 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // ../server/dist/chunk-NQN6AX3Q.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
+ function canUseSudo(allowSudo) {
97
+ if (allowSudo === false) return { allowed: false, reason: "sudo is not allowed" };
98
+ return { allowed: true };
99
+ }
100
+ var STANDARD_BLOCKED_PATHS = [
101
+ ".ssh",
102
+ ".gnupg",
103
+ ".gpg",
104
+ ".config/gigai",
105
+ ".aws",
106
+ ".azure",
107
+ ".gcloud",
108
+ ".kube",
109
+ ".docker"
110
+ ];
111
+ function canAccessPath(tier, resolvedPath, allowedPaths) {
112
+ switch (tier) {
113
+ case "strict": {
114
+ const paths = allowedPaths ?? [];
115
+ if (paths.length === 0) {
116
+ return { allowed: true };
117
+ }
118
+ const ok = paths.some((allowed) => {
119
+ const r = resolve(allowed);
120
+ return resolvedPath === r || resolvedPath.startsWith(r.endsWith("/") ? r : r + "/");
121
+ });
122
+ if (!ok) return { allowed: false, reason: `Path not within allowed directories: ${resolvedPath}` };
123
+ return { allowed: true };
124
+ }
125
+ case "standard": {
126
+ const home = homedir();
127
+ for (const blocked of STANDARD_BLOCKED_PATHS) {
128
+ const full = resolve(home, blocked);
129
+ if (resolvedPath === full || resolvedPath.startsWith(full + "/")) {
130
+ return { allowed: false, reason: `Path blocked by security policy: ${blocked}` };
131
+ }
132
+ }
133
+ return { allowed: true };
134
+ }
135
+ case "unrestricted":
136
+ return { allowed: true };
137
+ }
138
+ }
139
+ function shouldInjectSeparator(tier) {
140
+ return tier === "strict";
141
+ }
142
+
3
143
  // ../shared/dist/index.mjs
4
144
  import { randomBytes, createCipheriv, createDecipheriv } from "crypto";
5
145
  import { z } from "zod";
@@ -252,14 +392,25 @@ var ServerConfigSchema = z.object({
252
392
  host: z.string().default("0.0.0.0"),
253
393
  https: HttpsConfigSchema.optional()
254
394
  });
395
+ var SecurityTierSchema = z.enum(["strict", "standard", "unrestricted"]);
396
+ var SecurityConfigSchema = z.object({
397
+ default: SecurityTierSchema.default("strict"),
398
+ overrides: z.record(SecurityTierSchema).default({})
399
+ });
255
400
  var GigaiConfigSchema = z.object({
256
401
  serverName: z.string().optional(),
257
402
  server: ServerConfigSchema,
258
403
  auth: AuthConfigSchema,
259
- tools: z.array(ToolConfigSchema).default([])
404
+ tools: z.array(ToolConfigSchema).default([]),
405
+ security: SecurityConfigSchema.optional()
260
406
  });
261
407
 
262
408
  export {
409
+ getEffectiveTier,
410
+ canExecuteCommand,
411
+ canUseSudo,
412
+ canAccessPath,
413
+ shouldInjectSeparator,
263
414
  encrypt,
264
415
  decrypt,
265
416
  generateEncryptionKey,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  execCommandSafe
4
- } from "./chunk-GIUPSQGA.js";
4
+ } from "./chunk-WUAGWZNA.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-FR2BGYIC.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-XL27JFUS.js";
23
25
 
24
26
  // ../server/dist/index.mjs
25
27
  import { parseArgs } from "util";
@@ -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-F2MZGP6N-WUIBVJZF.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-FTLLT7JC-3DA3AAA7.js");
808
814
  const builtinConfig = entry.config.config ?? {};
809
815
  switch (entry.config.builtin) {
810
816
  case "filesystem": {
@@ -955,10 +961,11 @@ async function execRoutes(server) {
955
961
  }, async (request) => {
956
962
  const { tool, args, timeout } = request.body;
957
963
  const entry = server.registry.get(tool);
964
+ const tier = getEffectiveTier(server.securityConfig, tool);
958
965
  if (entry.type === "builtin") {
959
- return handleBuiltin(entry.config, args);
966
+ return handleBuiltin(entry.config, args, tier);
960
967
  }
961
- const result = await server.executor.execute(entry, args, timeout);
968
+ const result = await server.executor.execute(entry, args, timeout, tier);
962
969
  return result;
963
970
  });
964
971
  server.post("/exec/mcp", {
@@ -992,65 +999,65 @@ async function execRoutes(server) {
992
999
  };
993
1000
  });
994
1001
  }
995
- async function handleBuiltin(config, args) {
1002
+ async function handleBuiltin(config, args, tier) {
996
1003
  const builtinConfig = config.config ?? {};
997
1004
  switch (config.builtin) {
998
1005
  // Legacy combined filesystem tool
999
1006
  case "filesystem": {
1000
- const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1007
+ const allowedPaths = builtinConfig.allowedPaths;
1001
1008
  const subcommand = args[0];
1002
1009
  const target = args[1] ?? ".";
1003
1010
  switch (subcommand) {
1004
1011
  case "read":
1005
- return { stdout: await readFileSafe(target, allowedPaths), stderr: "", exitCode: 0, durationMs: 0 };
1012
+ return { stdout: await readFileSafe(target, allowedPaths ?? [], tier), stderr: "", exitCode: 0, durationMs: 0 };
1006
1013
  case "list":
1007
- return { stdout: JSON.stringify(await listDirSafe(target, allowedPaths), null, 2), stderr: "", exitCode: 0, durationMs: 0 };
1014
+ return { stdout: JSON.stringify(await listDirSafe(target, allowedPaths ?? [], tier), null, 2), stderr: "", exitCode: 0, durationMs: 0 };
1008
1015
  case "search":
1009
- return { stdout: JSON.stringify(await searchFilesSafe(target, args[2] ?? ".*", allowedPaths), null, 2), stderr: "", exitCode: 0, durationMs: 0 };
1016
+ return { stdout: JSON.stringify(await searchFilesSafe(target, args[2] ?? ".*", allowedPaths ?? [], tier), null, 2), stderr: "", exitCode: 0, durationMs: 0 };
1010
1017
  default:
1011
1018
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, `Unknown filesystem subcommand: ${subcommand}. Use: read, list, search`);
1012
1019
  }
1013
1020
  }
1014
1021
  // Legacy shell tool
1015
1022
  case "shell": {
1016
- const allowlist = builtinConfig.allowlist ?? [];
1017
- const allowSudo = builtinConfig.allowSudo ?? false;
1023
+ const allowlist = builtinConfig.allowlist;
1024
+ const allowSudo = builtinConfig.allowSudo;
1018
1025
  const command = args[0];
1019
1026
  if (!command) {
1020
1027
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "No command specified");
1021
1028
  }
1022
- const result = await execCommandSafe(command, args.slice(1), { allowlist, allowSudo });
1029
+ const result = await execCommandSafe(command, args.slice(1), { allowlist, allowSudo }, tier);
1023
1030
  return { ...result, durationMs: 0 };
1024
1031
  }
1025
1032
  // --- New builtins ---
1026
1033
  case "read": {
1027
- const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1028
- return { ...await readBuiltin(args, allowedPaths), durationMs: 0 };
1034
+ const allowedPaths = builtinConfig.allowedPaths;
1035
+ return { ...await readBuiltin(args, allowedPaths ?? [], tier), durationMs: 0 };
1029
1036
  }
1030
1037
  case "write": {
1031
- const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1032
- return { ...await writeBuiltin(args, allowedPaths), durationMs: 0 };
1038
+ const allowedPaths = builtinConfig.allowedPaths;
1039
+ return { ...await writeBuiltin(args, allowedPaths ?? [], tier), durationMs: 0 };
1033
1040
  }
1034
1041
  case "edit": {
1035
- const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1036
- return { ...await editBuiltin(args, allowedPaths), durationMs: 0 };
1042
+ const allowedPaths = builtinConfig.allowedPaths;
1043
+ return { ...await editBuiltin(args, allowedPaths ?? [], tier), durationMs: 0 };
1037
1044
  }
1038
1045
  case "glob": {
1039
- const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1040
- return { ...await globBuiltin(args, allowedPaths), durationMs: 0 };
1046
+ const allowedPaths = builtinConfig.allowedPaths;
1047
+ return { ...await globBuiltin(args, allowedPaths ?? [], tier), durationMs: 0 };
1041
1048
  }
1042
1049
  case "grep": {
1043
- const allowedPaths = builtinConfig.allowedPaths ?? ["."];
1044
- return { ...await grepBuiltin(args, allowedPaths), durationMs: 0 };
1050
+ const allowedPaths = builtinConfig.allowedPaths;
1051
+ return { ...await grepBuiltin(args, allowedPaths ?? [], tier), durationMs: 0 };
1045
1052
  }
1046
1053
  case "bash": {
1047
- const allowlist = builtinConfig.allowlist ?? [];
1048
- const allowSudo = builtinConfig.allowSudo ?? false;
1054
+ const allowlist = builtinConfig.allowlist;
1055
+ const allowSudo = builtinConfig.allowSudo;
1049
1056
  const command = args[0];
1050
1057
  if (!command) {
1051
1058
  throw new GigaiError(ErrorCode.VALIDATION_ERROR, "No command specified");
1052
1059
  }
1053
- const result = await execCommandSafe(command, args.slice(1), { allowlist, allowSudo });
1060
+ const result = await execCommandSafe(command, args.slice(1), { allowlist, allowSudo }, tier);
1054
1061
  return { ...result, durationMs: 0 };
1055
1062
  }
1056
1063
  default:
@@ -1224,6 +1231,7 @@ async function createServer(opts) {
1224
1231
  await server.register(registryPlugin, { config });
1225
1232
  await server.register(executorPlugin);
1226
1233
  await server.register(mcpPlugin, { config });
1234
+ server.decorate("securityConfig", config.security);
1227
1235
  if (configPath) {
1228
1236
  await server.register(cronPlugin, { configPath });
1229
1237
  }
@@ -1486,6 +1494,15 @@ async function runInit() {
1486
1494
  default: "7443"
1487
1495
  });
1488
1496
  const port = parseInt(portStr, 10);
1497
+ const securityTier = await select({
1498
+ message: "Security tier:",
1499
+ choices: [
1500
+ { name: "Strict \u2014 allowlist-only, explicit path restrictions (most secure)", value: "strict" },
1501
+ { name: "Standard \u2014 denylist blocks catastrophic commands, home dir open (recommended)", value: "standard" },
1502
+ { name: "Unrestricted \u2014 no restrictions, development only", value: "unrestricted" }
1503
+ ],
1504
+ default: "standard"
1505
+ });
1489
1506
  const selectedBuiltins = await checkbox({
1490
1507
  message: "Built-in tools to enable:",
1491
1508
  choices: [
@@ -1495,35 +1512,59 @@ async function runInit() {
1495
1512
  });
1496
1513
  const tools = [];
1497
1514
  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 }
1515
+ const restrictPaths = await confirm({
1516
+ message: "Restrict filesystem to specific paths?",
1517
+ default: false
1509
1518
  });
1519
+ if (restrictPaths) {
1520
+ const pathsStr = await input({
1521
+ message: "Allowed paths (comma-separated):",
1522
+ default: process.env.HOME ?? "~"
1523
+ });
1524
+ const allowedPaths = pathsStr.split(",").map((p) => p.trim());
1525
+ tools.push({
1526
+ type: "builtin",
1527
+ name: "fs",
1528
+ builtin: "filesystem",
1529
+ description: "Read, list, and search files",
1530
+ config: { allowedPaths }
1531
+ });
1532
+ } else {
1533
+ tools.push({
1534
+ type: "builtin",
1535
+ name: "fs",
1536
+ builtin: "filesystem",
1537
+ description: "Read, list, and search files",
1538
+ config: {}
1539
+ });
1540
+ }
1510
1541
  }
1511
1542
  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"
1543
+ const restrictCommands = await confirm({
1544
+ message: "Restrict shell to a command allowlist?",
1545
+ default: false
1515
1546
  });
1516
- const allowlist = allowlistStr.split(",").map((c) => c.trim());
1517
- const allowSudo = await confirm({
1518
- message: "Allow sudo?",
1547
+ const shellConfig = {};
1548
+ if (restrictCommands) {
1549
+ const allowlistStr = await input({
1550
+ message: "Allowed commands (comma-separated):",
1551
+ default: "ls,cat,head,tail,grep,find,wc,echo,date,whoami,pwd,git,npm,node"
1552
+ });
1553
+ shellConfig.allowlist = allowlistStr.split(",").map((c) => c.trim());
1554
+ }
1555
+ const blockSudo = await confirm({
1556
+ message: "Block sudo?",
1519
1557
  default: false
1520
1558
  });
1559
+ if (blockSudo) {
1560
+ shellConfig.allowSudo = false;
1561
+ }
1521
1562
  tools.push({
1522
1563
  type: "builtin",
1523
1564
  name: "shell",
1524
1565
  builtin: "shell",
1525
- description: "Execute allowed shell commands",
1526
- config: { allowlist, allowSudo }
1566
+ description: "Execute shell commands",
1567
+ config: shellConfig
1527
1568
  });
1528
1569
  }
1529
1570
  const configFilePath = await detectClaudeDesktopConfig();
@@ -1603,7 +1644,8 @@ async function runInit() {
1603
1644
  pairingTtlSeconds: 300,
1604
1645
  sessionTtlSeconds: 14400
1605
1646
  },
1606
- tools
1647
+ tools,
1648
+ security: { default: securityTier, overrides: {} }
1607
1649
  };
1608
1650
  const configPath = resolve4("gigai.config.json");
1609
1651
  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-FR2BGYIC.js";
13
+ import "./chunk-XL27JFUS.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.0";
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-SDKK67CH.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-WUAGWZNA.js";
5
+ import "./chunk-XL27JFUS.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.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "bin": {