@geekbeer/minion 2.59.0 → 2.60.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.
@@ -0,0 +1,279 @@
1
+ /**
2
+ * CLI Permission Abstraction Layer
3
+ *
4
+ * Provides a unified interface for reading/writing permission configurations
5
+ * across multiple AI coding CLI tools:
6
+ * - Claude Code: ~/.claude/settings.local.json (JSON)
7
+ * - Gemini CLI: ~/.gemini/settings.json (JSON)
8
+ * - Codex CLI: ~/.codex/config.toml (TOML)
9
+ */
10
+
11
+ const fs = require('fs')
12
+ const path = require('path')
13
+
14
+ // ── CLI Definitions ──────────────────────────────────────────────────────────
15
+
16
+ const CLI_DEFS = {
17
+ 'claude-code': {
18
+ label: 'Claude Code',
19
+ configDir: '.claude',
20
+ // Read from settings.json (bundled sync target) + merge settings.local.json (user overrides)
21
+ readFiles: ['settings.json', 'settings.local.json'],
22
+ // Write to settings.local.json to survive server restarts (settings.json is overwritten by syncPermissions)
23
+ writeFile: 'settings.local.json',
24
+ format: 'json',
25
+ },
26
+ 'gemini': {
27
+ label: 'Gemini CLI',
28
+ configDir: '.gemini',
29
+ readFiles: ['settings.json'],
30
+ writeFile: 'settings.json',
31
+ format: 'json',
32
+ },
33
+ 'codex': {
34
+ label: 'Codex CLI',
35
+ configDir: '.codex',
36
+ readFiles: ['config.toml'],
37
+ writeFile: 'config.toml',
38
+ format: 'toml',
39
+ },
40
+ }
41
+
42
+ // ── TOML Helpers (minimal, for Codex config.toml) ────────────────────────────
43
+
44
+ /**
45
+ * Parse a minimal TOML file to extract permissions.allow and permissions.deny arrays.
46
+ * Only supports the subset needed for Codex CLI config.
47
+ */
48
+ function parseTomlPermissions(content) {
49
+ const allow = []
50
+ const deny = []
51
+
52
+ let currentSection = ''
53
+ for (const rawLine of content.split('\n')) {
54
+ const line = rawLine.trim()
55
+ if (!line || line.startsWith('#')) continue
56
+
57
+ // Section header: [permissions] or [permissions.default] etc.
58
+ const sectionMatch = line.match(/^\[([^\]]+)\]$/)
59
+ if (sectionMatch) {
60
+ currentSection = sectionMatch[1]
61
+ continue
62
+ }
63
+
64
+ if (currentSection !== 'permissions') continue
65
+
66
+ // Key = value
67
+ const kvMatch = line.match(/^(\w+)\s*=\s*(.+)$/)
68
+ if (!kvMatch) continue
69
+ const [, key, rawValue] = kvMatch
70
+
71
+ if (key === 'allow' || key === 'deny') {
72
+ const arr = parseTomlArray(rawValue)
73
+ if (key === 'allow') allow.push(...arr)
74
+ else deny.push(...arr)
75
+ }
76
+ }
77
+
78
+ return { allow, deny }
79
+ }
80
+
81
+ /**
82
+ * Parse a TOML inline array: ["a", "b", "c"]
83
+ */
84
+ function parseTomlArray(raw) {
85
+ const trimmed = raw.trim()
86
+ if (!trimmed.startsWith('[') || !trimmed.endsWith(']')) return []
87
+ const inner = trimmed.slice(1, -1)
88
+ const items = []
89
+ // Match quoted strings
90
+ const re = /"([^"]*?)"|'([^']*?)'/g
91
+ let m
92
+ while ((m = re.exec(inner)) !== null) {
93
+ items.push(m[1] ?? m[2])
94
+ }
95
+ return items
96
+ }
97
+
98
+ /**
99
+ * Write permissions into a TOML file, preserving non-permissions content.
100
+ */
101
+ function writeTomlPermissions(existingContent, { allow, deny }) {
102
+ const lines = existingContent ? existingContent.split('\n') : []
103
+ const outputLines = []
104
+ let inPermissionsSection = false
105
+ let permissionsWritten = false
106
+
107
+ for (const line of lines) {
108
+ const trimmed = line.trim()
109
+ const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/)
110
+
111
+ if (sectionMatch) {
112
+ if (sectionMatch[1] === 'permissions') {
113
+ inPermissionsSection = true
114
+ // Write our new permissions section
115
+ outputLines.push('[permissions]')
116
+ outputLines.push(`allow = [${allow.map(s => `"${s}"`).join(', ')}]`)
117
+ outputLines.push(`deny = [${deny.map(s => `"${s}"`).join(', ')}]`)
118
+ permissionsWritten = true
119
+ continue
120
+ } else {
121
+ inPermissionsSection = false
122
+ }
123
+ }
124
+
125
+ if (inPermissionsSection) continue // skip old permission lines
126
+ outputLines.push(line)
127
+ }
128
+
129
+ // If no existing [permissions] section, append one
130
+ if (!permissionsWritten) {
131
+ if (outputLines.length > 0 && outputLines[outputLines.length - 1] !== '') {
132
+ outputLines.push('')
133
+ }
134
+ outputLines.push('[permissions]')
135
+ outputLines.push(`allow = [${allow.map(s => `"${s}"`).join(', ')}]`)
136
+ outputLines.push(`deny = [${deny.map(s => `"${s}"`).join(', ')}]`)
137
+ }
138
+
139
+ return outputLines.join('\n')
140
+ }
141
+
142
+ // ── JSON Helpers ─────────────────────────────────────────────────────────────
143
+
144
+ function readJsonPermissions(filePath) {
145
+ try {
146
+ const content = fs.readFileSync(filePath, 'utf-8')
147
+ const data = JSON.parse(content)
148
+ const perms = data.permissions || {}
149
+ return {
150
+ allow: Array.isArray(perms.allow) ? perms.allow : [],
151
+ deny: Array.isArray(perms.deny) ? perms.deny : [],
152
+ }
153
+ } catch {
154
+ return { allow: [], deny: [] }
155
+ }
156
+ }
157
+
158
+ function writeJsonPermissions(filePath, { allow, deny }) {
159
+ let data = {}
160
+ try {
161
+ data = JSON.parse(fs.readFileSync(filePath, 'utf-8'))
162
+ } catch {
163
+ // File doesn't exist or is invalid — start fresh
164
+ }
165
+ data.permissions = { allow, deny }
166
+ fs.mkdirSync(path.dirname(filePath), { recursive: true })
167
+ fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + '\n', 'utf-8')
168
+ }
169
+
170
+ // ── Public API ───────────────────────────────────────────────────────────────
171
+
172
+ /**
173
+ * Detect which CLIs have config directories present.
174
+ * @param {string} homeDir
175
+ * @returns {string[]} Array of cli_type strings
176
+ */
177
+ function detectInstalledClis(homeDir) {
178
+ return Object.entries(CLI_DEFS)
179
+ .filter(([, def]) => fs.existsSync(path.join(homeDir, def.configDir)))
180
+ .map(([type]) => type)
181
+ }
182
+
183
+ /**
184
+ * Read effective permissions for a specific CLI.
185
+ * For Claude Code, merges settings.json + settings.local.json.
186
+ * @param {string} homeDir
187
+ * @param {string} cliType
188
+ * @returns {{ cli_type: string, label: string, allow: string[], deny: string[], config_path: string } | null}
189
+ */
190
+ function getPermissions(homeDir, cliType) {
191
+ const def = CLI_DEFS[cliType]
192
+ if (!def) return null
193
+
194
+ const configDir = path.join(homeDir, def.configDir)
195
+ if (!fs.existsSync(configDir)) return null
196
+
197
+ let mergedAllow = []
198
+ let mergedDeny = []
199
+
200
+ for (const file of def.readFiles) {
201
+ const filePath = path.join(configDir, file)
202
+ if (!fs.existsSync(filePath)) continue
203
+
204
+ if (def.format === 'json') {
205
+ const perms = readJsonPermissions(filePath)
206
+ // Later files override earlier ones (settings.local.json overrides settings.json)
207
+ if (perms.allow.length > 0 || perms.deny.length > 0) {
208
+ mergedAllow = perms.allow
209
+ mergedDeny = perms.deny
210
+ }
211
+ } else if (def.format === 'toml') {
212
+ try {
213
+ const content = fs.readFileSync(filePath, 'utf-8')
214
+ const perms = parseTomlPermissions(content)
215
+ if (perms.allow.length > 0 || perms.deny.length > 0) {
216
+ mergedAllow = perms.allow
217
+ mergedDeny = perms.deny
218
+ }
219
+ } catch {
220
+ // Skip unreadable files
221
+ }
222
+ }
223
+ }
224
+
225
+ return {
226
+ cli_type: cliType,
227
+ label: def.label,
228
+ allow: mergedAllow,
229
+ deny: mergedDeny,
230
+ config_path: path.join(configDir, def.writeFile),
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Read permissions for all detected CLIs.
236
+ * @param {string} homeDir
237
+ * @returns {Array<{ cli_type: string, label: string, allow: string[], deny: string[], config_path: string }>}
238
+ */
239
+ function getAllPermissions(homeDir) {
240
+ const results = []
241
+ for (const cliType of Object.keys(CLI_DEFS)) {
242
+ const perms = getPermissions(homeDir, cliType)
243
+ if (perms) results.push(perms)
244
+ }
245
+ return results
246
+ }
247
+
248
+ /**
249
+ * Update permissions for a specific CLI.
250
+ * @param {string} homeDir
251
+ * @param {string} cliType
252
+ * @param {{ allow: string[], deny: string[] }} permissions
253
+ */
254
+ function setPermissions(homeDir, cliType, { allow, deny }) {
255
+ const def = CLI_DEFS[cliType]
256
+ if (!def) throw new Error(`Unsupported CLI type: ${cliType}`)
257
+
258
+ const configDir = path.join(homeDir, def.configDir)
259
+ const filePath = path.join(configDir, def.writeFile)
260
+
261
+ fs.mkdirSync(configDir, { recursive: true })
262
+
263
+ if (def.format === 'json') {
264
+ writeJsonPermissions(filePath, { allow, deny })
265
+ } else if (def.format === 'toml') {
266
+ let existing = ''
267
+ try { existing = fs.readFileSync(filePath, 'utf-8') } catch {}
268
+ const newContent = writeTomlPermissions(existing, { allow, deny })
269
+ fs.writeFileSync(filePath, newContent, 'utf-8')
270
+ }
271
+ }
272
+
273
+ module.exports = {
274
+ detectInstalledClis,
275
+ getPermissions,
276
+ getAllPermissions,
277
+ setPermissions,
278
+ CLI_DEFS,
279
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Permission management endpoints
3
+ *
4
+ * Provides read/write access to CLI tool permission configurations.
5
+ * Supports Claude Code, Gemini CLI, and Codex CLI.
6
+ *
7
+ * Endpoints:
8
+ * GET /api/config/permissions - Get permissions for all detected CLIs
9
+ * GET /api/config/permissions/:cli_type - Get permissions for a specific CLI
10
+ * POST /api/config/permissions - Update permissions for a specific CLI
11
+ */
12
+
13
+ const { verifyToken } = require('../lib/auth')
14
+ const { config } = require('../config')
15
+ const { getAllPermissions, getPermissions, setPermissions, CLI_DEFS } = require('../lib/permissions')
16
+
17
+ /**
18
+ * Register permission routes as Fastify plugin
19
+ * @param {import('fastify').FastifyInstance} fastify
20
+ */
21
+ async function permissionRoutes(fastify) {
22
+
23
+ // GET /api/config/permissions - Get permissions for all detected CLIs
24
+ fastify.get('/api/config/permissions', async (request, reply) => {
25
+ if (!verifyToken(request)) {
26
+ reply.code(401)
27
+ return { success: false, error: 'Unauthorized' }
28
+ }
29
+
30
+ const permissions = getAllPermissions(config.HOME_DIR)
31
+ return { success: true, permissions }
32
+ })
33
+
34
+ // GET /api/config/permissions/:cli_type - Get permissions for a specific CLI
35
+ fastify.get('/api/config/permissions/:cli_type', async (request, reply) => {
36
+ if (!verifyToken(request)) {
37
+ reply.code(401)
38
+ return { success: false, error: 'Unauthorized' }
39
+ }
40
+
41
+ const { cli_type } = request.params
42
+ if (!CLI_DEFS[cli_type]) {
43
+ reply.code(400)
44
+ return { success: false, error: `Unsupported CLI type: ${cli_type}. Supported: ${Object.keys(CLI_DEFS).join(', ')}` }
45
+ }
46
+
47
+ const perms = getPermissions(config.HOME_DIR, cli_type)
48
+ if (!perms) {
49
+ reply.code(404)
50
+ return { success: false, error: `CLI ${cli_type} is not installed on this minion` }
51
+ }
52
+
53
+ return { success: true, ...perms }
54
+ })
55
+
56
+ // POST /api/config/permissions - Update permissions for a specific CLI
57
+ fastify.post('/api/config/permissions', async (request, reply) => {
58
+ if (!verifyToken(request)) {
59
+ reply.code(401)
60
+ return { success: false, error: 'Unauthorized' }
61
+ }
62
+
63
+ const { cli_type, allow, deny } = request.body || {}
64
+
65
+ if (!cli_type) {
66
+ reply.code(400)
67
+ return { success: false, error: 'cli_type is required' }
68
+ }
69
+ if (!CLI_DEFS[cli_type]) {
70
+ reply.code(400)
71
+ return { success: false, error: `Unsupported CLI type: ${cli_type}. Supported: ${Object.keys(CLI_DEFS).join(', ')}` }
72
+ }
73
+ if (!Array.isArray(allow) || !Array.isArray(deny)) {
74
+ reply.code(400)
75
+ return { success: false, error: 'allow and deny must be arrays' }
76
+ }
77
+
78
+ try {
79
+ setPermissions(config.HOME_DIR, cli_type, { allow, deny })
80
+
81
+ // Read back effective permissions to confirm
82
+ const updated = getPermissions(config.HOME_DIR, cli_type)
83
+ console.log(`[Permissions] Updated ${cli_type}: allow=${allow.length}, deny=${deny.length}`)
84
+
85
+ return { success: true, ...updated }
86
+ } catch (err) {
87
+ reply.code(500)
88
+ return { success: false, error: err.message }
89
+ }
90
+ })
91
+ }
92
+
93
+ module.exports = { permissionRoutes }
@@ -204,6 +204,46 @@ Changes via the config API take effect immediately (no restart required).
204
204
 
205
205
  Allowed keys: `LLM_COMMAND`, `REFLECTION_TIME`
206
206
 
207
+ ### Permissions
208
+
209
+ CLIツール(Claude Code, Gemini CLI, Codex CLI)のパーミッション管理。
210
+ CLIツールは自身の設定ファイルを直接編集できないため、このAPIを経由して更新する。
211
+
212
+ | Method | Endpoint | Description |
213
+ |--------|----------|-------------|
214
+ | GET | `/api/config/permissions` | Get permissions for all detected CLIs |
215
+ | GET | `/api/config/permissions/:cli_type` | Get permissions for a specific CLI |
216
+ | POST | `/api/config/permissions` | Update permissions. Body: `{cli_type, allow, deny}` |
217
+
218
+ Supported `cli_type`: `claude-code`, `gemini`, `codex`
219
+
220
+ **GET response**:
221
+ ```json
222
+ {
223
+ "success": true,
224
+ "permissions": [
225
+ {
226
+ "cli_type": "claude-code",
227
+ "label": "Claude Code",
228
+ "allow": ["Bash", "Read", "Write", "Edit"],
229
+ "deny": ["Bash(sudo *)"],
230
+ "config_path": "/home/minion/.claude/settings.local.json"
231
+ }
232
+ ]
233
+ }
234
+ ```
235
+
236
+ **POST body**:
237
+ ```json
238
+ {
239
+ "cli_type": "claude-code",
240
+ "allow": ["Bash", "Read", "Write", "Edit", "WebSearch"],
241
+ "deny": ["Bash(sudo *)", "Bash(rm -rf *)"]
242
+ }
243
+ ```
244
+
245
+ Note: Claude Code の場合、書き込み先は `settings.local.json`(サーバー再起動時に上書きされない)。
246
+
207
247
  ### Commands
208
248
 
209
249
  | Method | Endpoint | Description |
package/linux/server.js CHANGED
@@ -67,6 +67,7 @@ const { variableRoutes } = require('../core/routes/variables')
67
67
  const { memoryRoutes } = require('../core/routes/memory')
68
68
  const { dailyLogRoutes } = require('../core/routes/daily-logs')
69
69
  const { sudoersRoutes } = require('../core/routes/sudoers')
70
+ const { permissionRoutes } = require('../core/routes/permissions')
70
71
 
71
72
  // Linux-specific routes
72
73
  const { commandRoutes, getProcessManager, getAllowedCommands } = require('./routes/commands')
@@ -268,6 +269,7 @@ async function registerAllRoutes(app) {
268
269
  await app.register(memoryRoutes)
269
270
  await app.register(dailyLogRoutes)
270
271
  await app.register(sudoersRoutes)
272
+ await app.register(permissionRoutes)
271
273
 
272
274
  // Linux-specific routes
273
275
  await app.register(commandRoutes)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "2.59.0",
3
+ "version": "2.60.0",
4
4
  "description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
5
5
  "main": "linux/server.js",
6
6
  "bin": {
package/rules/core.md CHANGED
@@ -45,7 +45,27 @@ minion-cli --version # バージョン確認
45
45
 
46
46
  `http://localhost:8080` — 認証: `Authorization: Bearer $API_TOKEN`
47
47
 
48
- 主なカテゴリ: Health, Skills, Workflows, Executions, Terminal, Files, Commands
48
+ 主なカテゴリ: Health, Skills, Workflows, Executions, Terminal, Files, Commands, Permissions
49
+
50
+ #### Permission Management
51
+
52
+ CLIツール(Claude Code, Gemini CLI, Codex CLI)のパーミッション(allow/denyリスト)をAPI経由で読み書きできる。
53
+ CLIツールは自身の設定ファイルを直接編集できないため、このAPIを使用すること。
54
+
55
+ ```bash
56
+ # 全CLIのパーミッション取得
57
+ curl -H "Authorization: Bearer $API_TOKEN" http://localhost:8080/api/config/permissions
58
+
59
+ # 特定CLIのパーミッション取得
60
+ curl -H "Authorization: Bearer $API_TOKEN" http://localhost:8080/api/config/permissions/claude-code
61
+
62
+ # パーミッション更新
63
+ curl -X POST -H "Authorization: Bearer $API_TOKEN" -H "Content-Type: application/json" \
64
+ http://localhost:8080/api/config/permissions \
65
+ -d '{"cli_type": "claude-code", "allow": ["Bash", "Read", "Write", "Edit"], "deny": ["Bash(sudo *)"]}'
66
+ ```
67
+
68
+ 対応CLI: `claude-code` (.claude/settings.local.json), `gemini` (.gemini/settings.json), `codex` (.codex/config.toml)
49
69
 
50
70
  ### HQ API
51
71
 
package/win/server.js CHANGED
@@ -50,6 +50,7 @@ const { authRoutes } = require('../core/routes/auth')
50
50
  const { variableRoutes } = require('../core/routes/variables')
51
51
  const { memoryRoutes } = require('../core/routes/memory')
52
52
  const { dailyLogRoutes } = require('../core/routes/daily-logs')
53
+ const { permissionRoutes } = require('../core/routes/permissions')
53
54
 
54
55
  // Validate configuration
55
56
  validate()
@@ -201,6 +202,7 @@ async function registerRoutes(app) {
201
202
  await app.register(variableRoutes)
202
203
  await app.register(memoryRoutes)
203
204
  await app.register(dailyLogRoutes)
205
+ await app.register(permissionRoutes)
204
206
 
205
207
  // Windows-specific routes
206
208
  await app.register(commandRoutes)