atabey 0.0.14 → 0.0.16

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.
Files changed (159) hide show
  1. package/LICENSE +672 -21
  2. package/PRIVACY.md +8 -14
  3. package/README.md +183 -25
  4. package/bin/cli.js +72 -5
  5. package/dist/framework-mcp/src/index.js +145 -13
  6. package/dist/framework-mcp/src/index.js.map +1 -1
  7. package/dist/framework-mcp/src/tools/control_plane/registry.d.ts +1 -1
  8. package/dist/framework-mcp/src/tools/control_plane/registry.js +52 -4
  9. package/dist/framework-mcp/src/tools/control_plane/registry.js.map +1 -1
  10. package/dist/framework-mcp/src/tools/definitions.js +1 -1
  11. package/dist/framework-mcp/src/tools/file_system/read_file.js +5 -0
  12. package/dist/framework-mcp/src/tools/file_system/read_file.js.map +1 -1
  13. package/dist/framework-mcp/src/tools/messaging/approve_operation.d.ts +10 -8
  14. package/dist/framework-mcp/src/tools/messaging/approve_operation.js +12 -12
  15. package/dist/framework-mcp/src/tools/messaging/approve_operation.js.map +1 -1
  16. package/dist/framework-mcp/src/tools/messaging/ask_human.d.ts +8 -8
  17. package/dist/framework-mcp/src/tools/messaging/ask_human.js +8 -8
  18. package/dist/framework-mcp/src/tools/messaging/send_message.js +19 -3
  19. package/dist/framework-mcp/src/tools/messaging/send_message.js.map +1 -1
  20. package/dist/framework-mcp/src/tools/observability/check_ports.d.ts +1 -0
  21. package/dist/framework-mcp/src/tools/observability/check_ports.js +21 -7
  22. package/dist/framework-mcp/src/tools/observability/check_ports.js.map +1 -1
  23. package/dist/framework-mcp/src/tools/schemas.d.ts +4 -4
  24. package/dist/framework-mcp/src/tools/search/grep_search.js +11 -4
  25. package/dist/framework-mcp/src/tools/search/grep_search.js.map +1 -1
  26. package/dist/framework-mcp/src/tools/search/list_dir.js +3 -0
  27. package/dist/framework-mcp/src/tools/search/list_dir.js.map +1 -1
  28. package/dist/framework-mcp/src/tools/shell/run_command.js +185 -93
  29. package/dist/framework-mcp/src/tools/shell/run_command.js.map +1 -1
  30. package/dist/framework-mcp/src/utils/auth.d.ts +26 -2
  31. package/dist/framework-mcp/src/utils/auth.js +74 -7
  32. package/dist/framework-mcp/src/utils/auth.js.map +1 -1
  33. package/dist/framework-mcp/src/utils/compliance.js +45 -13
  34. package/dist/framework-mcp/src/utils/compliance.js.map +1 -1
  35. package/dist/framework-mcp/src/utils/discipline.d.ts +1 -0
  36. package/dist/framework-mcp/src/utils/discipline.js +42 -3
  37. package/dist/framework-mcp/src/utils/discipline.js.map +1 -1
  38. package/dist/framework-mcp/src/utils/human-in-loop.d.ts +35 -2
  39. package/dist/framework-mcp/src/utils/human-in-loop.js +114 -10
  40. package/dist/framework-mcp/src/utils/human-in-loop.js.map +1 -1
  41. package/dist/framework-mcp/src/utils/loop-detector.js +3 -3
  42. package/dist/framework-mcp/src/utils/loop-detector.js.map +1 -1
  43. package/dist/framework-mcp/src/utils/permissions.d.ts +67 -3
  44. package/dist/framework-mcp/src/utils/permissions.js +179 -43
  45. package/dist/framework-mcp/src/utils/permissions.js.map +1 -1
  46. package/dist/framework-mcp/src/utils/prompt-injection.d.ts +20 -0
  47. package/dist/framework-mcp/src/utils/prompt-injection.js +64 -0
  48. package/dist/framework-mcp/src/utils/prompt-injection.js.map +1 -0
  49. package/dist/framework-mcp/src/utils/quality.js +4 -3
  50. package/dist/framework-mcp/src/utils/quality.js.map +1 -1
  51. package/dist/framework-mcp/src/utils/silent-router.d.ts +4 -2
  52. package/dist/framework-mcp/src/utils/silent-router.js +35 -9
  53. package/dist/framework-mcp/src/utils/silent-router.js.map +1 -1
  54. package/dist/framework-mcp/tests/tools/file_system/permissions.test.js +174 -71
  55. package/dist/framework-mcp/tests/tools/file_system/permissions.test.js.map +1 -1
  56. package/dist/framework-mcp/tests/tools/messaging/send_message.test.js +36 -30
  57. package/dist/framework-mcp/tests/tools/messaging/send_message.test.js.map +1 -1
  58. package/dist/framework-mcp/tests/tools/observability/check_ports.test.js +26 -12
  59. package/dist/framework-mcp/tests/tools/observability/check_ports.test.js.map +1 -1
  60. package/dist/framework-mcp/tests/tools/shell/run_command.test.js +38 -15
  61. package/dist/framework-mcp/tests/tools/shell/run_command.test.js.map +1 -1
  62. package/dist/framework-mcp/tests/utils/human-in-loop.test.js +152 -4
  63. package/dist/framework-mcp/tests/utils/human-in-loop.test.js.map +1 -1
  64. package/dist/framework-mcp/tests/utils/loop-detector.test.js +0 -2
  65. package/dist/framework-mcp/tests/utils/loop-detector.test.js.map +1 -1
  66. package/dist/framework-mcp/tests/utils/prompt-injection.test.d.ts +1 -0
  67. package/dist/framework-mcp/tests/utils/prompt-injection.test.js +43 -0
  68. package/dist/framework-mcp/tests/utils/prompt-injection.test.js.map +1 -0
  69. package/dist/src/cli/commands/dashboard.js +167 -2
  70. package/dist/src/cli/commands/dashboard.js.map +1 -1
  71. package/dist/src/cli/commands/init/scaffold-standards.js +1 -1
  72. package/dist/src/cli/commands/init/scaffold-standards.js.map +1 -1
  73. package/dist/src/cli/commands/init.js +14 -7
  74. package/dist/src/cli/commands/init.js.map +1 -1
  75. package/dist/src/cli/commands/log.js +8 -5
  76. package/dist/src/cli/commands/log.js.map +1 -1
  77. package/dist/src/cli/commands/mcp.js +3 -1
  78. package/dist/src/cli/commands/mcp.js.map +1 -1
  79. package/dist/src/cli/commands/orchestrate.d.ts +4 -4
  80. package/dist/src/cli/commands/orchestrate.js +14 -2
  81. package/dist/src/cli/commands/orchestrate.js.map +1 -1
  82. package/dist/src/cli/commands/script.js +8 -1
  83. package/dist/src/cli/commands/script.js.map +1 -1
  84. package/dist/src/cli/commands/status.js +21 -1
  85. package/dist/src/cli/commands/status.js.map +1 -1
  86. package/dist/src/cli/utils/memory.js +6 -5
  87. package/dist/src/cli/utils/memory.js.map +1 -1
  88. package/dist/src/cli/utils/schemas.d.ts +2 -2
  89. package/dist/src/contracts/tasks.d.ts +4 -4
  90. package/dist/src/modules/contracts/api.d.ts +6 -6
  91. package/dist/src/modules/engines/agent-executor.js +41 -20
  92. package/dist/src/modules/engines/agent-executor.js.map +1 -1
  93. package/dist/src/modules/engines/agent-loop.d.ts +5 -5
  94. package/dist/src/modules/engines/agent-loop.js +5 -5
  95. package/dist/src/modules/engines/evaluation-engine.d.ts +9 -2
  96. package/dist/src/modules/engines/evaluation-engine.js +77 -9
  97. package/dist/src/modules/engines/evaluation-engine.js.map +1 -1
  98. package/dist/src/modules/engines/quality-gate.js +4 -2
  99. package/dist/src/modules/engines/quality-gate.js.map +1 -1
  100. package/dist/src/modules/engines/risk-engine.d.ts +17 -1
  101. package/dist/src/modules/engines/risk-engine.js +98 -3
  102. package/dist/src/modules/engines/risk-engine.js.map +1 -1
  103. package/dist/src/modules/engines/routing-engine.d.ts +26 -1
  104. package/dist/src/modules/engines/routing-engine.js +97 -1
  105. package/dist/src/modules/engines/routing-engine.js.map +1 -1
  106. package/dist/src/modules/providers/shared.js +2 -3
  107. package/dist/src/modules/providers/shared.js.map +1 -1
  108. package/dist/src/shared/pii.js +1 -1
  109. package/dist/src/shared/pii.js.map +1 -1
  110. package/dist/src/shared/storage.d.ts +60 -0
  111. package/dist/src/shared/storage.js +77 -3
  112. package/dist/src/shared/storage.js.map +1 -1
  113. package/dist/tests/cli/commands/dashboard-coverage.test.js +3 -1
  114. package/dist/tests/cli/commands/dashboard-coverage.test.js.map +1 -1
  115. package/dist/tests/modules/engines/agent-executor.test.js +1 -1
  116. package/dist/tests/modules/engines/agent-executor.test.js.map +1 -1
  117. package/dist/tests/modules/engines/evaluation-engine.test.js +2 -2
  118. package/dist/tests/modules/engines/evaluation-engine.test.js.map +1 -1
  119. package/dist/tests/status-cost.test.js +2 -0
  120. package/dist/tests/status-cost.test.js.map +1 -1
  121. package/dist/tests/storage-operations.test.js +31 -4
  122. package/dist/tests/storage-operations.test.js.map +1 -1
  123. package/dist/vitest.config.js +6 -4
  124. package/dist/vitest.config.js.map +1 -1
  125. package/framework-mcp/README.md +18 -32
  126. package/framework-mcp/dist/dashboard/assets/index-B-Eu7_J9.js +1260 -0
  127. package/framework-mcp/dist/dashboard/index.html +1 -1
  128. package/framework-mcp/dist/framework-mcp/src/index.js +145 -13
  129. package/framework-mcp/dist/framework-mcp/src/tools/control_plane/registry.js +52 -4
  130. package/framework-mcp/dist/framework-mcp/src/tools/definitions.js +1 -1
  131. package/framework-mcp/dist/framework-mcp/src/tools/file_system/read_file.js +5 -0
  132. package/framework-mcp/dist/framework-mcp/src/tools/messaging/approve_operation.js +12 -12
  133. package/framework-mcp/dist/framework-mcp/src/tools/messaging/ask_human.js +8 -8
  134. package/framework-mcp/dist/framework-mcp/src/tools/messaging/send_message.js +19 -3
  135. package/framework-mcp/dist/framework-mcp/src/tools/observability/check_ports.js +21 -7
  136. package/framework-mcp/dist/framework-mcp/src/tools/search/grep_search.js +11 -4
  137. package/framework-mcp/dist/framework-mcp/src/tools/search/list_dir.js +3 -0
  138. package/framework-mcp/dist/framework-mcp/src/tools/shell/run_command.js +185 -93
  139. package/framework-mcp/dist/framework-mcp/src/utils/auth.js +74 -7
  140. package/framework-mcp/dist/framework-mcp/src/utils/compliance.js +45 -13
  141. package/framework-mcp/dist/framework-mcp/src/utils/discipline.js +42 -3
  142. package/framework-mcp/dist/framework-mcp/src/utils/human-in-loop.js +114 -10
  143. package/framework-mcp/dist/framework-mcp/src/utils/loop-detector.js +3 -3
  144. package/framework-mcp/dist/framework-mcp/src/utils/permissions.js +179 -43
  145. package/framework-mcp/dist/framework-mcp/src/utils/prompt-injection.js +63 -0
  146. package/framework-mcp/dist/framework-mcp/src/utils/quality.js +4 -3
  147. package/framework-mcp/dist/framework-mcp/src/utils/silent-router.js +35 -9
  148. package/framework-mcp/dist/src/cli/utils/memory.js +6 -5
  149. package/framework-mcp/dist/src/modules/engines/evaluation-engine.js +170 -0
  150. package/framework-mcp/dist/src/modules/engines/quality-gate.js +4 -2
  151. package/framework-mcp/dist/src/modules/engines/risk-engine.js +98 -3
  152. package/framework-mcp/dist/src/modules/engines/routing-engine.js +97 -1
  153. package/framework-mcp/dist/src/modules/providers/shared.js +2 -3
  154. package/framework-mcp/dist/src/shared/pii.js +1 -1
  155. package/framework-mcp/dist/src/shared/storage.js +77 -3
  156. package/framework-mcp/package.json +21 -18
  157. package/mcp.json +3 -2
  158. package/package.json +4 -5
  159. package/framework-mcp/dist/dashboard/assets/index-B2mYld0c.js +0 -1221
@@ -1,102 +1,175 @@
1
- import { exec } from "child_process";
1
+ import { spawn } from "child_process";
2
2
  import { Metrics } from "../../utils/metrics.js";
3
+ import { resolveActiveAgent, getAgentTier } from "../../utils/permissions.js";
4
+ import { resolveFrameworkDir } from "../../utils/security.js";
5
+ import path from "path";
6
+ // Each entry defines the executable and its allowed args prefix
3
7
  const COMMAND_ALLOW_LIST = [
4
8
  // Package managers
5
- "npm test",
6
- "npm run",
7
- "npm install",
8
- "npm ci",
9
- "npm audit",
10
- "npx vitest",
11
- "npx tsc",
12
- "npx eslint",
13
- "yarn test",
14
- "yarn run",
15
- "yarn install",
16
- "pnpm test",
17
- "pnpm run",
18
- "pnpm install",
9
+ { cmd: "npm", args: ["test"] },
10
+ { cmd: "npm", args: ["run"] },
11
+ { cmd: "npm", args: ["install"] },
12
+ { cmd: "npm", args: ["ci"] },
13
+ { cmd: "npm", args: ["audit"] },
14
+ { cmd: "npx", args: ["vitest"] },
15
+ { cmd: "npx", args: ["tsc"] },
16
+ { cmd: "npx", args: ["eslint"] },
17
+ { cmd: "yarn", args: ["test"] },
18
+ { cmd: "yarn", args: ["run"] },
19
+ { cmd: "yarn", args: ["install"] },
20
+ { cmd: "pnpm", args: ["test"] },
21
+ { cmd: "pnpm", args: ["run"] },
22
+ { cmd: "pnpm", args: ["install"] },
19
23
  // Git
20
- "git status",
21
- "git diff",
22
- "git log",
23
- "git branch",
24
- "git stash",
25
- "git add",
26
- "git commit",
27
- "git push",
28
- "git pull",
29
- "git fetch",
30
- "git merge",
31
- "git rebase",
32
- "git checkout",
33
- "git reset",
24
+ { cmd: "git", args: ["status"] },
25
+ { cmd: "git", args: ["diff"] },
26
+ { cmd: "git", args: ["log"] },
27
+ { cmd: "git", args: ["branch"] },
28
+ { cmd: "git", args: ["stash"] },
29
+ { cmd: "git", args: ["add"] },
30
+ { cmd: "git", args: ["commit"] },
31
+ { cmd: "git", args: ["push"] },
32
+ { cmd: "git", args: ["pull"] },
33
+ { cmd: "git", args: ["fetch"] },
34
+ { cmd: "git", args: ["merge"] },
35
+ { cmd: "git", args: ["rebase"] },
36
+ { cmd: "git", args: ["checkout"] },
37
+ { cmd: "git", args: ["reset"] },
34
38
  // Build tools
35
- "npm run build",
36
- "npx tsc",
37
- "npx vite build",
38
- "npx webpack",
39
- "npx rollup",
40
- "npx esbuild",
41
- "make",
42
- "cmake",
43
- "dotnet test",
44
- "dotnet format",
45
- "dotnet build",
46
- "dotnet run",
47
- "dotnet restore",
48
- "go test",
49
- "go fmt",
50
- "go build",
51
- "go run",
52
- "go vet",
53
- "go mod",
54
- "pytest",
55
- "ruff check",
56
- "ruff format",
57
- "mvn",
58
- "./gradlew",
59
- "cargo test",
60
- "cargo build",
61
- "cargo check",
62
- "cargo fmt",
63
- // File operations (safe) — NOTE: "rm -rf" and "rm -r" intentionally excluded.
64
- // Only "rmdir" is allowed to prevent dangerous commands like "rm -rf /".
65
- "mkdir",
66
- "cp",
67
- "mv",
68
- "rmdir",
69
- "cat",
70
- "ls",
71
- "touch",
72
- "head",
73
- "tail",
74
- "wc",
75
- "grep",
76
- "find",
77
- "sort",
78
- "uniq",
79
- "echo",
80
- "pwd",
81
- "which",
82
- "file",
83
- "du",
84
- "df",
39
+ { cmd: "make" },
40
+ { cmd: "cmake" },
41
+ { cmd: "dotnet", args: ["test"] },
42
+ { cmd: "dotnet", args: ["format"] },
43
+ { cmd: "dotnet", args: ["build"] },
44
+ { cmd: "dotnet", args: ["run"] },
45
+ { cmd: "dotnet", args: ["restore"] },
46
+ { cmd: "go", args: ["test"] },
47
+ { cmd: "go", args: ["fmt"] },
48
+ { cmd: "go", args: ["build"] },
49
+ { cmd: "go", args: ["run"] },
50
+ { cmd: "go", args: ["vet"] },
51
+ { cmd: "go", args: ["mod"] },
52
+ { cmd: "pytest" },
53
+ { cmd: "ruff", args: ["check"] },
54
+ { cmd: "ruff", args: ["format"] },
55
+ { cmd: "mvn" },
56
+ { cmd: "./gradlew" },
57
+ { cmd: "cargo", args: ["test"] },
58
+ { cmd: "cargo", args: ["build"] },
59
+ { cmd: "cargo", args: ["check"] },
60
+ { cmd: "cargo", args: ["fmt"] },
61
+ // File operations (safe) — spawn prevents shell injection
62
+ { cmd: "mkdir" },
63
+ { cmd: "cp" },
64
+ { cmd: "mv" },
65
+ { cmd: "rmdir" },
66
+ { cmd: "cat" },
67
+ { cmd: "ls" },
68
+ { cmd: "touch" },
69
+ { cmd: "head" },
70
+ { cmd: "tail" },
71
+ { cmd: "wc" },
72
+ { cmd: "grep" },
73
+ { cmd: "find" },
74
+ { cmd: "sort" },
75
+ { cmd: "uniq" },
76
+ { cmd: "echo" },
77
+ { cmd: "pwd" },
78
+ { cmd: "which" },
79
+ { cmd: "file" },
80
+ { cmd: "du" },
81
+ { cmd: "df" },
85
82
  ];
86
- const TIMEOUT = 30000; // 30 seconds
83
+ const TIMEOUT = parseInt(process.env.MCP_COMMAND_TIMEOUT_MS || "30000", 10);
84
+ /**
85
+ * Checks if a parsed command represents a state-mutating (write) operation.
86
+ */
87
+ function isWriteCommand(parsed) {
88
+ const cmd = parsed.cmd;
89
+ const firstArg = parsed.args[0] || "";
90
+ const writeCmds = ["mkdir", "cp", "mv", "rmdir", "touch", "make", "cmake", "./gradlew", "mvn"];
91
+ if (writeCmds.includes(cmd))
92
+ return true;
93
+ if (cmd === "npm" && ["install", "ci", "audit", "run"].includes(firstArg))
94
+ return true;
95
+ if (cmd === "yarn" && ["install", "run"].includes(firstArg))
96
+ return true;
97
+ if (cmd === "pnpm" && ["install", "run"].includes(firstArg))
98
+ return true;
99
+ if (cmd === "git" && ["add", "commit", "push", "pull", "merge", "rebase", "checkout", "reset"].includes(firstArg))
100
+ return true;
101
+ if (cmd === "dotnet" && ["build", "run", "restore"].includes(firstArg))
102
+ return true;
103
+ if (cmd === "go" && ["build", "run", "mod"].includes(firstArg))
104
+ return true;
105
+ if (cmd === "cargo" && ["build", "check"].includes(firstArg))
106
+ return true;
107
+ return false;
108
+ }
109
+ /**
110
+ * Parse a command string into executable + args array.
111
+ * Uses spawn for safety (no shell injection).
112
+ */
113
+ function parseCommand(cmdString) {
114
+ const parts = cmdString.trim().split(/\s+/);
115
+ if (parts.length === 0)
116
+ return null;
117
+ return { cmd: parts[0], args: parts.slice(1) };
118
+ }
119
+ /**
120
+ * Check if a parsed command matches the allow list.
121
+ */
122
+ function isAllowed(parsed) {
123
+ return COMMAND_ALLOW_LIST.some(entry => {
124
+ if (entry.cmd !== parsed.cmd)
125
+ return false;
126
+ if (!entry.args)
127
+ return true; // Any args allowed for this cmd
128
+ if (!parsed.args || parsed.args.length < entry.args.length)
129
+ return false;
130
+ // Check that args start with the allowed prefix
131
+ return entry.args.every((arg, i) => parsed.args[i] === arg);
132
+ });
133
+ }
87
134
  export function handleRunCommand(projectRoot, args) {
88
135
  const command = args.command;
89
- const isAllowed = COMMAND_ALLOW_LIST.some(allowedCmd => command.startsWith(allowedCmd));
90
- if (!isAllowed) {
91
- const errorMsg = `Command not allowed: "${command}". Only commands starting with the following are allowed: ${COMMAND_ALLOW_LIST.join(", ")}`;
136
+ // Parse command into executable + args
137
+ const parsed = parseCommand(command);
138
+ if (!parsed) {
139
+ return Promise.resolve({
140
+ content: [{ type: "text", text: "ERROR: Empty command." }],
141
+ isError: true,
142
+ });
143
+ }
144
+ // Resolve active agent and tier to check command permission
145
+ const frameworkDir = resolveFrameworkDir(projectRoot);
146
+ const absoluteFrameworkPath = path.isAbsolute(frameworkDir)
147
+ ? frameworkDir
148
+ : path.resolve(projectRoot, frameworkDir);
149
+ const activeAgent = resolveActiveAgent(absoluteFrameworkPath);
150
+ if (activeAgent) {
151
+ const tier = getAgentTier(activeAgent);
152
+ if (tier === "recon" && isWriteCommand(parsed)) {
153
+ return Promise.resolve({
154
+ isError: true,
155
+ content: [{
156
+ type: "text",
157
+ text: `[RBAC] Permission Denied: Agent ${activeAgent} (tier: recon) is not allowed to run write/build command "${command}". Recon agents are restricted to read-only commands.`
158
+ }]
159
+ });
160
+ }
161
+ }
162
+ if (!isAllowed(parsed)) {
163
+ const allowedList = COMMAND_ALLOW_LIST.map(e => e.args ? `${e.cmd} ${e.args.join(" ")}` : e.cmd);
164
+ const errorMsg = `Command not allowed: "${command}". Only the following are allowed: ${allowedList.join(", ")}`;
92
165
  Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command} (denied)`, errorMsg);
93
166
  return Promise.resolve({
94
167
  content: [{ type: "text", text: `ERROR: ${errorMsg}` }],
95
168
  isError: true,
96
169
  });
97
170
  }
98
- // Harden to prevent shell command injection / chaining
99
- const hasShellMetacharacters = /[;&|><$`\n\r]/.test(command);
171
+ // Reject any args containing shell metacharacters
172
+ const hasShellMetacharacters = parsed.args.some(a => /[;&|><$`\n\r]/.test(a));
100
173
  if (hasShellMetacharacters) {
101
174
  const errorMsg = "Command rejected: Shell metacharacters are forbidden to prevent command injection.";
102
175
  Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command} (denied: metacharacters)`, errorMsg);
@@ -106,12 +179,30 @@ export function handleRunCommand(projectRoot, args) {
106
179
  });
107
180
  }
108
181
  return new Promise((resolve) => {
109
- exec(command, { cwd: projectRoot, timeout: TIMEOUT }, (error, stdout, stderr) => {
182
+ const child = spawn(parsed.cmd, parsed.args, {
183
+ cwd: projectRoot,
184
+ timeout: TIMEOUT,
185
+ shell: false,
186
+ stdio: ["ignore", "pipe", "pipe"],
187
+ });
188
+ let stdout = "";
189
+ let stderr = "";
190
+ child.stdout.on("data", (data) => { stdout += data.toString(); });
191
+ child.stderr.on("data", (data) => { stderr += data.toString(); });
192
+ child.on("error", (err) => {
193
+ const errorMsg = `Failed to start command: ${err.message}`;
194
+ Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command}`, errorMsg);
195
+ resolve({
196
+ content: [{ type: "text", text: `ERROR: ${errorMsg}` }],
197
+ isError: true,
198
+ });
199
+ });
200
+ child.on("close", (code) => {
110
201
  const output = stdout + stderr;
111
202
  const tokens = Metrics.estimateTokens(output);
112
203
  Metrics.logUsage(projectRoot, "@mcp", `run_shell_command: ${command}`, tokens);
113
- if (error) {
114
- const errorMsg = `Command failed with exit code ${error.code}: ${error.message}.`;
204
+ if (code !== 0) {
205
+ const errorMsg = `Command failed with exit code ${code}.`;
115
206
  Metrics.logError(projectRoot, "@mcp", `run_shell_command: ${command}`, errorMsg);
116
207
  resolve({
117
208
  content: [{ type: "text", text: `ERROR: ${errorMsg}. Output: ${output}` }],
@@ -121,11 +212,12 @@ export function handleRunCommand(projectRoot, args) {
121
212
  }
122
213
  // Truncate long outputs
123
214
  const MAX_OUTPUT_LENGTH = 5000;
124
- let truncatedOutput = output;
125
- if (output.length > MAX_OUTPUT_LENGTH) {
126
- truncatedOutput = output.substring(0, MAX_OUTPUT_LENGTH) + "... [TRUNCATED] ..."; // Simplified
127
- }
128
- resolve({ content: [{ type: "text", text: truncatedOutput }] });
215
+ const truncatedOutput = output.length > MAX_OUTPUT_LENGTH
216
+ ? output.substring(0, MAX_OUTPUT_LENGTH) + "... [TRUNCATED] ..."
217
+ : output;
218
+ resolve({
219
+ content: [{ type: "text", text: truncatedOutput }],
220
+ });
129
221
  });
130
222
  });
131
223
  }
@@ -5,12 +5,18 @@
5
5
  * When MCP_AUTH_TOKEN is set, all MCP and API requests require
6
6
  * Authorization: Bearer <token> header.
7
7
  *
8
+ * Supports multi-user setup via MCP_AUTH_USERS:
9
+ * MCP_AUTH_USERS=alice:key1,bob:key2,cmdr:key3
10
+ *
11
+ * Each user gets their own API key. Tool calls and audit logs
12
+ * are tagged with the authenticated user's name.
13
+ *
8
14
  * Environment Variables:
9
- * MCP_AUTH_TOKEN - API key for authentication (optional)
15
+ * MCP_AUTH_TOKEN - Master API key for admin access (optional)
10
16
  * MCP_AUTH_USERS - Comma-separated list of user:token pairs (optional)
11
17
  * Example: "alice:key1,bob:key2"
12
18
  *
13
- * If neither is set, authentication is disabled (open access).
19
+ * If neither is set, authentication is disabled (open access / anonymous).
14
20
  */
15
21
  let authUsers = [];
16
22
  let masterToken = null;
@@ -27,14 +33,14 @@ export function initAuth() {
27
33
  }).filter(u => u.token);
28
34
  }
29
35
  if (masterToken) {
30
- console.error(`[AUTH] Master token configured (${masterToken.length} chars)`);
36
+ process.stderr.write(`[AUTH] Master token configured (${masterToken.length} chars)\n`);
31
37
  }
32
38
  if (authUsers.length > 0) {
33
- console.error(`[AUTH] ${authUsers.length} user(s) configured: ${authUsers.map(u => u.name).join(", ")}`);
39
+ process.stderr.write(`[AUTH] ${authUsers.length} user(s) configured: ${authUsers.map(u => u.name).join(", ")}\n`);
34
40
  }
35
41
  if (!masterToken && authUsers.length === 0) {
36
- console.error("[AUTH] No authentication configured - OPEN ACCESS");
37
- console.error("[AUTH] Set MCP_AUTH_TOKEN=<key> or MCP_AUTH_USERS=user1:key1,user2:key2 to enable");
42
+ process.stderr.write("[AUTH] No authentication configured - OPEN ACCESS (anonymous users)\n");
43
+ process.stderr.write("[AUTH] Set MCP_AUTH_TOKEN=<key> or MCP_AUTH_USERS=user1:key1,user2:key2 to enable\n");
38
44
  }
39
45
  }
40
46
  /**
@@ -43,7 +49,7 @@ export function initAuth() {
43
49
  */
44
50
  export function authenticate(req) {
45
51
  if (!masterToken && authUsers.length === 0) {
46
- // Auth disabled - allow all
52
+ // Auth disabled - allow all as anonymous
47
53
  return { authenticated: true, user: "anonymous" };
48
54
  }
49
55
  const authHeader = req.headers["authorization"];
@@ -64,6 +70,53 @@ export function authenticate(req) {
64
70
  }
65
71
  return { authenticated: false, user: "" };
66
72
  }
73
+ /**
74
+ * Authenticate using a raw token string (for MCP stdio mode).
75
+ * In stdio mode, the user identity is set via environment.
76
+ */
77
+ export function authenticateToken(token) {
78
+ if (!masterToken && authUsers.length === 0) {
79
+ return { authenticated: true, user: "anonymous" };
80
+ }
81
+ // Check master token
82
+ if (masterToken && token === masterToken) {
83
+ return { authenticated: true, user: "admin" };
84
+ }
85
+ // Check user tokens
86
+ for (const user of authUsers) {
87
+ if (token === user.token) {
88
+ return { authenticated: true, user: user.name };
89
+ }
90
+ }
91
+ return { authenticated: false, user: "" };
92
+ }
93
+ /**
94
+ * Get the current authenticated user identity for stdio mode.
95
+ * Reads from environment or returns "anonymous".
96
+ * For HTTP/SSE mode, use authenticate() on the request.
97
+ */
98
+ export function getCurrentUser() {
99
+ // Stdio mode: user identity comes from env or git config
100
+ const envUser = process.env.MCP_USER;
101
+ if (envUser)
102
+ return envUser;
103
+ // Try to get from git config
104
+ try {
105
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
106
+ const { execSync } = require("child_process");
107
+ const gitUser = execSync("git config user.name", { encoding: "utf8" }).trim();
108
+ if (gitUser)
109
+ return gitUser;
110
+ }
111
+ catch {
112
+ // git not available
113
+ }
114
+ // Try system user
115
+ const systemUser = process.env.USER || process.env.USERNAME;
116
+ if (systemUser)
117
+ return systemUser;
118
+ return "anonymous";
119
+ }
67
120
  /**
68
121
  * Generate authentication headers for MCP client configuration.
69
122
  */
@@ -76,3 +129,17 @@ export function getAuthHeader(user, token) {
76
129
  export function isAuthEnabled() {
77
130
  return !!masterToken || authUsers.length > 0;
78
131
  }
132
+ /**
133
+ * Get list of configured users (without tokens).
134
+ */
135
+ export function getConfiguredUsers() {
136
+ if (authUsers.length > 0) {
137
+ return authUsers.map(u => u.name);
138
+ }
139
+ if (masterToken) {
140
+ return ["admin"];
141
+ }
142
+ return ["anonymous"];
143
+ }
144
+ // Auto-initialize on load
145
+ initAuth();
@@ -2,6 +2,8 @@ import ts from "typescript";
2
2
  import fs from "fs";
3
3
  import path from "path";
4
4
  import { resolveFrameworkDir } from "./security.js";
5
+ import { resolveActiveAgent } from "./permissions.js";
6
+ import { createApprovalRequest, getApprovalStatus } from "./human-in-loop.js";
5
7
  /**
6
8
  * Enterprise Compliance Guardrail
7
9
  * Checks content against corporate standards using AST analysis before allowing file mutations.
@@ -57,7 +59,7 @@ export function verifyCorporateCompliance(content, filePath) {
57
59
  filePath.includes("shared/fs.ts") ||
58
60
  filePath.includes("shared/lock.ts") ||
59
61
  filePath.includes("quality-gate.ts") ||
60
- filePath.includes("providers/shared.ts");
62
+ filePath.includes("shell/run_command.ts");
61
63
  if (!isExempt) {
62
64
  errors.push(`[ERROR] Corporate Compliance Breach: Direct 'child_process' usage is forbidden at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}. Use secure framework APIs.`);
63
65
  }
@@ -85,7 +87,10 @@ export function verifyCorporateCompliance(content, filePath) {
85
87
  // Lock files, core atomic fs wrapper, and mcp configs are exempt
86
88
  const args = node.arguments;
87
89
  const firstArgText = args.length > 0 ? args[0].getText(sourceFile).toLowerCase() : "";
88
- const isExemptFile = filePath.includes("shared/fs.ts") || filePath.includes("src/cli/commands/mcp.ts");
90
+ const isExemptFile = filePath.includes("shared/fs.ts") ||
91
+ filePath.includes("src/cli/commands/mcp.ts") ||
92
+ filePath.includes("utils/compliance.ts") ||
93
+ filePath.includes("tools/messaging/send_message.ts");
89
94
  if (!firstArgText.includes("lock") && !isExemptFile) {
90
95
  errors.push(`[ERROR] Corporate Compliance Breach: Raw 'fs.${prop.name.text}' mutation detected at line ${sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1}. Use atomic utilities instead.`);
91
96
  }
@@ -204,7 +209,9 @@ export async function verifyRiskAndAwaitApproval(projectRoot, content, filePath)
204
209
  const statePath = path.join(absoluteFrameworkPath, "memory", "state.json");
205
210
  const messagesDir = path.join(absoluteFrameworkPath, "messages");
206
211
  const managerMsgPath = path.join(messagesDir, "manager.json");
207
- let activeAgent = null;
212
+ const activeAgent = resolveActiveAgent(absoluteFrameworkPath);
213
+ if (!activeAgent)
214
+ throw new Error(`Security Exception: High-risk operation blocked. ${assessment.reason}.`);
208
215
  let traceId = "UNKNOWN";
209
216
  if (fs.existsSync(statePath)) {
210
217
  try {
@@ -214,21 +221,25 @@ export async function verifyRiskAndAwaitApproval(projectRoot, content, filePath)
214
221
  }
215
222
  catch { /* ignore */ }
216
223
  }
224
+ // Check if already approved via human-in-loop
225
+ const existingStatus = getApprovalStatus(traceId);
226
+ if (existingStatus?.status === "APPROVED") {
227
+ return;
228
+ }
229
+ if (existingStatus?.status === "REJECTED" || existingStatus?.status === "EXPIRED") {
230
+ throw new Error(`Security Exception: High-risk operation was ${existingStatus.status}.`);
231
+ }
232
+ // Register in human-in-loop if not already present
233
+ if (!existingStatus) {
234
+ createApprovalRequest(traceId, "file_mutation", activeAgent, 80, assessment.reason || "High-risk operation detected", { filePath });
235
+ }
217
236
  let statusData = {};
218
237
  if (fs.existsSync(statusPath)) {
219
238
  try {
220
239
  statusData = JSON.parse(fs.readFileSync(statusPath, "utf8"));
221
- for (const [agentName, info] of Object.entries(statusData)) {
222
- if (info.state === "EXECUTING") {
223
- activeAgent = agentName.startsWith("@") ? agentName : `@${agentName}`;
224
- break;
225
- }
226
- }
227
240
  }
228
241
  catch { /* ignore */ }
229
242
  }
230
- if (!activeAgent)
231
- throw new Error(`Security Exception: High-risk operation blocked. ${assessment.reason}.`);
232
243
  const statusKey = activeAgent.replace("@", "");
233
244
  const originalTask = statusData[statusKey]?.task || "Executing task";
234
245
  statusData[statusKey] = {
@@ -253,12 +264,16 @@ export async function verifyRiskAndAwaitApproval(projectRoot, content, filePath)
253
264
  requiresApproval: true,
254
265
  action: `MUTATION:${filePath}`
255
266
  };
267
+ // Append to managerMsgPath for backward compatibility / legacy alerts
256
268
  try {
257
269
  fs.appendFileSync(managerMsgPath, JSON.stringify(alertMsg) + "\n");
258
270
  }
259
271
  catch (err) {
260
272
  statusData[statusKey] = { state: "EXECUTING", task: originalTask };
261
- fs.writeFileSync(statusPath, JSON.stringify(statusData, null, 2));
273
+ try {
274
+ fs.writeFileSync(statusPath, JSON.stringify(statusData, null, 2));
275
+ }
276
+ catch { /* ignore */ }
262
277
  throw new Error("Security Exception: Failed to queue approval request.", { cause: err });
263
278
  }
264
279
  const pollIntervalMs = 500;
@@ -266,6 +281,20 @@ export async function verifyRiskAndAwaitApproval(projectRoot, content, filePath)
266
281
  const start = Date.now();
267
282
  while (Date.now() - start < timeoutMs) {
268
283
  await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
284
+ // Check if approved via human-in-loop (e.g. approve_operation tool call)
285
+ const checkStatus = getApprovalStatus(traceId);
286
+ if (checkStatus?.status === "APPROVED") {
287
+ statusData[statusKey] = { state: "EXECUTING", task: originalTask };
288
+ try {
289
+ fs.writeFileSync(statusPath, JSON.stringify(statusData, null, 2));
290
+ }
291
+ catch { /* ignore */ }
292
+ return;
293
+ }
294
+ else if (checkStatus?.status === "REJECTED" || checkStatus?.status === "EXPIRED") {
295
+ throw new Error(`Security Exception: High-risk operation was explicitly ${checkStatus.status}.`);
296
+ }
297
+ // Check legacy managerMsgPath approval
269
298
  if (fs.existsSync(managerMsgPath)) {
270
299
  try {
271
300
  const lines = fs.readFileSync(managerMsgPath, "utf8").trim().split("\n");
@@ -276,7 +305,10 @@ export async function verifyRiskAndAwaitApproval(projectRoot, content, filePath)
276
305
  if (parsed.traceId === traceId && parsed.category === "ALERT" && parsed.action === `MUTATION:${filePath}`) {
277
306
  if (parsed.status === "APPROVED") {
278
307
  statusData[statusKey] = { state: "EXECUTING", task: originalTask };
279
- fs.writeFileSync(statusPath, JSON.stringify(statusData, null, 2));
308
+ try {
309
+ fs.writeFileSync(statusPath, JSON.stringify(statusData, null, 2));
310
+ }
311
+ catch { /* ignore */ }
280
312
  return;
281
313
  }
282
314
  else if (parsed.status === "PROCESSED" || parsed.status === "DENIED") {
@@ -156,8 +156,41 @@ export async function enforceDiscipline(agent, toolName, args) {
156
156
  });
157
157
  return null;
158
158
  }
159
+ // ─── Prompt Injection Patterns ─────────────────────────────────────────────
160
+ // Common adversarial patterns that attempt to override agent instructions.
161
+ // These are checked in tool responses to prevent exfiltrated injection payloads
162
+ // (e.g. from malicious file content, API responses, or external user input)
163
+ // from reaching the AI and overriding the system prompt.
164
+ const PROMPT_INJECTION_PATTERNS = [
165
+ { pattern: /ignore\s+(previous|prior|above|all)\s+instructions?/i, label: "ignore-instructions" },
166
+ { pattern: /disregard\s+(previous|prior|above|all)\s+instructions?/i, label: "disregard-instructions" },
167
+ { pattern: /forget\s+(everything|all|prior|previous)/i, label: "forget-prior" },
168
+ { pattern: /you\s+are\s+now\s+(a|an)\s+/i, label: "persona-override" },
169
+ { pattern: /\bsystem\s*:\s*(you|your|ignore|forget|act)/i, label: "system-prompt-inject" },
170
+ { pattern: /\bnew\s+instructions?\s*:/i, label: "new-instructions" },
171
+ { pattern: /\bact\s+as\s+(if\s+you\s+are|a|an)\s+/i, label: "act-as-override" },
172
+ { pattern: /\boverride\s+(safety|rules|guidelines|instructions)/i, label: "safety-override" },
173
+ { pattern: /\bdo\s+not\s+follow\s+(your|the|any)\s+(rules|instructions)/i, label: "rule-bypass" },
174
+ { pattern: /\bdeveloper\s+mode\s+(enabled|activated|on)/i, label: "developer-mode" },
175
+ { pattern: /\bjailbreak/i, label: "jailbreak" },
176
+ { pattern: /<\|.*?\|>/, label: "token-boundary-inject" },
177
+ { pattern: /\[INST\]|\[\/?SYS\]|<<SYS>>/, label: "llm-template-inject" },
178
+ ];
179
+ /**
180
+ * Scans a text block for known prompt injection patterns.
181
+ * Returns the matched pattern label if injection is detected, null otherwise.
182
+ */
183
+ function scanForPromptInjection(text) {
184
+ for (const { pattern, label } of PROMPT_INJECTION_PATTERNS) {
185
+ if (pattern.test(text)) {
186
+ return label;
187
+ }
188
+ }
189
+ return null;
190
+ }
159
191
  /**
160
192
  * Validate tool response content before returning to AI.
193
+ * Blocks: oversized responses, binary data, and prompt injection attempts.
161
194
  * Returns error message if blocked, null if allowed.
162
195
  */
163
196
  export function validateResponse(toolName, result) {
@@ -166,15 +199,21 @@ export function validateResponse(toolName, result) {
166
199
  for (const block of result.content) {
167
200
  if (block.type !== "text" || !block.text)
168
201
  continue;
169
- // Check response size
202
+ // 1. Check response size
170
203
  if (block.text.length > CONFIG.MAX_RESPONSE_SIZE) {
171
204
  return `[DISCIPLINE] Response too large (${(block.text.length / 1024).toFixed(1)}KB, max: ${(CONFIG.MAX_RESPONSE_SIZE / 1024).toFixed(1)}KB).`;
172
205
  }
173
- // Block binary/gibberish content in text responses
206
+ // 2. Block binary/gibberish content in text responses
174
207
  const nonPrintable = (block.text.match(/[^\x20-\x7E\n\r\t]/g) || []).length;
175
208
  if (nonPrintable > block.text.length * 0.3) {
176
209
  return `[DISCIPLINE] Response contains too much binary data (${nonPrintable} non-printable chars). Blocked.`;
177
210
  }
211
+ // 3. Prompt injection detection — scan for adversarial override patterns
212
+ // that may have been embedded in file content, API responses, or user input.
213
+ const injectionLabel = scanForPromptInjection(block.text);
214
+ if (injectionLabel) {
215
+ return `[DISCIPLINE] Prompt injection pattern detected in "${toolName}" response (pattern: ${injectionLabel}). Response blocked to protect agent integrity.`;
216
+ }
178
217
  }
179
218
  return null;
180
219
  }
@@ -202,7 +241,7 @@ export function getDisciplineStats(agent) {
202
241
  */
203
242
  export function getAllDisciplineStats() {
204
243
  const stats = {};
205
- for (const [agent, _discipline] of agentDiscipline) {
244
+ for (const [agent] of agentDiscipline) {
206
245
  stats[agent] = getDisciplineStats(agent);
207
246
  }
208
247
  return stats;