claude-bulkhead 0.2.2 → 0.3.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.
- package/CHANGELOG.md +51 -0
- package/README.es.md +142 -50
- package/README.ja.md +142 -50
- package/README.ko.md +143 -51
- package/README.md +142 -50
- package/README.pt-BR.md +142 -50
- package/README.zh-CN.md +154 -62
- package/hooks/hooks.json +68 -8
- package/package.json +5 -2
- package/src/agent-hook.js +162 -0
- package/src/agent-setup.js +256 -0
- package/src/agents.js +89 -0
- package/src/allow-cli.js +65 -0
- package/src/allow-hint.js +62 -0
- package/src/allow-once.js +152 -0
- package/src/auto-queue.js +93 -0
- package/src/bash-guards.js +36 -0
- package/src/basics.js +46 -27
- package/src/checkout-guard.js +342 -74
- package/src/cli-util.js +13 -1
- package/src/cli.js +40 -4
- package/src/config-check.js +21 -4
- package/src/config-cli.js +75 -10
- package/src/config.js +106 -9
- package/src/dev-servers.js +361 -61
- package/src/disk-guard.js +8 -8
- package/src/doctor-enforcement.js +273 -0
- package/src/doctor.js +19 -9
- package/src/edit-guard.js +196 -0
- package/src/events.js +30 -5
- package/src/explain.js +251 -0
- package/src/git-info.js +55 -0
- package/src/gitargs.js +82 -0
- package/src/hook.js +38 -6
- package/src/kill-extras.js +219 -0
- package/src/kill-guard.js +228 -77
- package/src/kill-owners.js +116 -0
- package/src/log-cli.js +15 -10
- package/src/mcp.js +131 -0
- package/src/memory-guard.js +126 -24
- package/src/memory.js +21 -2
- package/src/notices.js +95 -0
- package/src/orphan-reaper.js +58 -11
- package/src/port-cli.js +78 -3
- package/src/port-guard.js +80 -68
- package/src/port-kills.js +198 -0
- package/src/pressure.js +75 -0
- package/src/processes.js +44 -12
- package/src/scan-report.js +166 -0
- package/src/scan.js +87 -18
- package/src/secrets-guard.js +301 -0
- package/src/self-allow.js +73 -0
- package/src/session-info.js +121 -0
- package/src/session-state.js +176 -0
- package/src/sessions.js +109 -20
- package/src/shell.js +279 -19
- package/src/slot-cli.js +92 -21
- package/src/slots.js +61 -13
- package/src/stable-port.js +61 -0
- package/src/status.js +55 -17
- package/src/statusline.js +45 -16
- package/src/subagent-guard.js +24 -0
- package/src/top.js +36 -11
- package/src/wait-guard.js +96 -32
- package/src/worktree-cli.js +454 -0
- package/src/worktree-copy.js +82 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const { option } = require('./cli-util')
|
|
4
|
+
|
|
5
|
+
// `bulkhead hook --agent <name>`: the Bash guards for coding agents other than Claude Code. The
|
|
6
|
+
// agent pipes its pre-tool payload on stdin; the adapter maps it to the shape the guards read
|
|
7
|
+
// ({ tool_name, tool_input: { command }, cwd, session_id }), runs the same guards as the Claude
|
|
8
|
+
// Code PreToolUse hook, and answers in the agent's own block format. Anything unexpected lets
|
|
9
|
+
// the command through, and Copilot and Cursor treat a crash or bad output as a denial, so those
|
|
10
|
+
// two always get an explicit allow.
|
|
11
|
+
|
|
12
|
+
const str = v => (typeof v === 'string' && v ? v : null)
|
|
13
|
+
const obj = v => (v && typeof v === 'object' && !Array.isArray(v) ? v : null)
|
|
14
|
+
const json = v => JSON.stringify(v)
|
|
15
|
+
|
|
16
|
+
function asObject(v) {
|
|
17
|
+
if (typeof v !== 'string') return obj(v)
|
|
18
|
+
try { return obj(JSON.parse(v)) } catch { return null }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const sh = (command, cwd, session, extra = {}) =>
|
|
22
|
+
command ? { tool_name: 'Bash', tool_input: { command, ...extra }, cwd: cwd || undefined, session_id: session || undefined } : null
|
|
23
|
+
|
|
24
|
+
const powershell = input => (input ? { ...input, tool_name: 'PowerShell' } : null)
|
|
25
|
+
|
|
26
|
+
// Cline's run_commands takes a string, a list, { commands }, { command, args } or { cmd }.
|
|
27
|
+
function clineCommands(input) {
|
|
28
|
+
const quote = a => (/^[\w@%+=:,./-]+$/.test(a) ? a : `'${a.replace(/'/g, `'\\''`)}'`)
|
|
29
|
+
const one = c => (typeof c === 'string' ? c : obj(c) && str(c.command) ? [c.command, ...(Array.isArray(c.args) ? c.args.map(String) : [])].map(quote).join(' ') : null)
|
|
30
|
+
let list = []
|
|
31
|
+
if (typeof input === 'string') list = [input]
|
|
32
|
+
else if (Array.isArray(input)) list = input
|
|
33
|
+
else if (obj(input)) {
|
|
34
|
+
if ('commands' in input) list = Array.isArray(input.commands) ? input.commands : [input.commands]
|
|
35
|
+
else if ('command' in input) list = [input]
|
|
36
|
+
else if ('cmd' in input) list = [input.cmd]
|
|
37
|
+
}
|
|
38
|
+
return list.map(one).filter(Boolean).join('\n')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const resolveDir = (cwd, dir) => (str(dir) ? path.resolve(cwd || process.cwd(), dir) : cwd)
|
|
42
|
+
|
|
43
|
+
// How each agent's payload maps and how it wants a block (and, where silence is unsafe, an allow)
|
|
44
|
+
// answered. read() returns null for anything that is not a shell command.
|
|
45
|
+
const ADAPTERS = {
|
|
46
|
+
// https://developers.openai.com/codex/hooks: PreToolUse, tool_name "Bash", tool_input.command
|
|
47
|
+
codex: {
|
|
48
|
+
read: p => (p.tool_name === 'Bash' ? sh(str(obj(p.tool_input) && p.tool_input.command), p.cwd, p.session_id) : null),
|
|
49
|
+
block: msg => ({ out: json({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: msg } }) }),
|
|
50
|
+
},
|
|
51
|
+
// https://github.com/google-gemini/gemini-cli/blob/main/docs/hooks/reference.md: BeforeTool, run_shell_command
|
|
52
|
+
gemini: {
|
|
53
|
+
read: p => {
|
|
54
|
+
const ti = obj(p.tool_input)
|
|
55
|
+
if (p.tool_name !== 'run_shell_command' || !ti) return null
|
|
56
|
+
return sh(str(ti.command), resolveDir(p.cwd, ti.dir_path), p.session_id, ti.is_background ? { run_in_background: true } : {})
|
|
57
|
+
},
|
|
58
|
+
block: msg => ({ out: json({ decision: 'deny', reason: msg }) }),
|
|
59
|
+
},
|
|
60
|
+
// https://docs.github.com/en/copilot/reference/hooks-configuration: preToolUse, camelCase or
|
|
61
|
+
// (PreToolUse) snake_case payload; toolArgs may be a JSON string
|
|
62
|
+
copilot: {
|
|
63
|
+
read: p => {
|
|
64
|
+
const name = String(p.toolName || p.tool_name || '').toLowerCase()
|
|
65
|
+
if (name !== 'bash' && name !== 'powershell') return null
|
|
66
|
+
const args = asObject(p.toolArgs !== undefined ? p.toolArgs : p.tool_input)
|
|
67
|
+
const input = sh(str(args && args.command), p.cwd, p.sessionId || p.session_id)
|
|
68
|
+
return name === 'powershell' ? powershell(input) : input
|
|
69
|
+
},
|
|
70
|
+
block: msg => ({ out: json({ permissionDecision: 'deny', permissionDecisionReason: msg }) }),
|
|
71
|
+
allow: '',
|
|
72
|
+
},
|
|
73
|
+
// https://cursor.com/docs/hooks: beforeShellExecution { command, cwd }, answer { permission }
|
|
74
|
+
cursor: {
|
|
75
|
+
read: p => sh(str(p.command), str(p.cwd) || (Array.isArray(p.workspace_roots) && str(p.workspace_roots[0])), p.conversation_id),
|
|
76
|
+
block: msg => ({ out: json({ permission: 'deny', user_message: msg.split('\n')[0], agent_message: msg }) }),
|
|
77
|
+
allow: json({ permission: 'allow' }),
|
|
78
|
+
},
|
|
79
|
+
// Windsurf (Cascade) pre_run_command { tool_info: { command_line, cwd } }; exit 2 blocks
|
|
80
|
+
windsurf: {
|
|
81
|
+
read: p => {
|
|
82
|
+
const ti = obj(p.tool_info)
|
|
83
|
+
return ti ? sh(str(ti.command_line), ti.cwd, p.trajectory_id) : null
|
|
84
|
+
},
|
|
85
|
+
block: msg => ({ err: msg, code: 2 }),
|
|
86
|
+
},
|
|
87
|
+
// https://docs.factory.ai/reference/hooks-reference: PreToolUse, tool_name "Execute"
|
|
88
|
+
droid: {
|
|
89
|
+
read: p => (p.tool_name === 'Execute' ? sh(str(obj(p.tool_input) && p.tool_input.command), p.cwd, p.session_id) : null),
|
|
90
|
+
block: msg => ({ out: json({ hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: msg } }) }),
|
|
91
|
+
},
|
|
92
|
+
// https://docs.devin.ai/cli/extensibility/hooks/overview.md: PreToolUse, tool_name "exec"; exit 2 blocks
|
|
93
|
+
devin: {
|
|
94
|
+
read: p => (/^(?:exec|execute|bash|shell)$/i.test(String(p.tool_name)) ? sh(str(obj(p.tool_input) && p.tool_input.command), p.cwd, p.session_id) : null),
|
|
95
|
+
block: msg => ({ err: msg, code: 2 }),
|
|
96
|
+
},
|
|
97
|
+
// https://github.com/cline/cline/blob/main/sdk/examples/hooks/README.md: PreToolUse file hook, run_commands
|
|
98
|
+
cline: {
|
|
99
|
+
read: p => {
|
|
100
|
+
const call = obj(p.tool_call)
|
|
101
|
+
if (!call || call.name !== 'run_commands') return null
|
|
102
|
+
return sh(clineCommands(call.input), Array.isArray(p.workspaceRoots) ? p.workspaceRoots[0] : null, p.taskId)
|
|
103
|
+
},
|
|
104
|
+
block: msg => ({ out: json({ cancel: true, errorMessage: msg }) }),
|
|
105
|
+
allow: '{}',
|
|
106
|
+
},
|
|
107
|
+
// The opencode and Amp plugins that `bulkhead setup` writes pipe { command, cwd, session_id }
|
|
108
|
+
// and turn exit code 2 into a rejected tool call.
|
|
109
|
+
opencode: { read: p => sh(str(p.command), p.cwd, p.session_id), block: msg => ({ err: msg, code: 2 }) },
|
|
110
|
+
amp: { read: p => sh(str(p.command), p.cwd, p.session_id), block: msg => ({ err: msg, code: 2 }) },
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const AGENT_NAMES = Object.keys(ADAPTERS)
|
|
114
|
+
|
|
115
|
+
// The same guards, in the same order, as the Claude Code hook (bash-guards.js): the first block wins.
|
|
116
|
+
async function judge(input) {
|
|
117
|
+
const r = await require('./bash-guards').judge(input, { rewrite: false })
|
|
118
|
+
return r && r.block ? r.block : null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// raw: the payload text. Returns { out, err, code } to write and exit with; never throws.
|
|
122
|
+
async function run(agent, raw) {
|
|
123
|
+
const adapter = ADAPTERS[agent]
|
|
124
|
+
const allow = { out: adapter.allow || '', code: 0 }
|
|
125
|
+
try {
|
|
126
|
+
const input = adapter.read(JSON.parse(raw || '{}'))
|
|
127
|
+
if (!input) return allow
|
|
128
|
+
process.env.BULKHEAD_AGENT = agent
|
|
129
|
+
const message = await judge(input)
|
|
130
|
+
if (!message) return allow
|
|
131
|
+
const answer = adapter.block(message.endsWith('\n') ? message.slice(0, -1) : message)
|
|
132
|
+
return { out: '', err: '', code: 0, ...answer }
|
|
133
|
+
} catch {
|
|
134
|
+
return allow
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function readStdin(stream) {
|
|
139
|
+
return new Promise(resolve => {
|
|
140
|
+
if (stream.isTTY) return resolve('')
|
|
141
|
+
let raw = ''
|
|
142
|
+
stream.setEncoding('utf8')
|
|
143
|
+
stream.on('data', d => (raw += d))
|
|
144
|
+
stream.on('end', () => resolve(raw))
|
|
145
|
+
stream.on('error', () => resolve(''))
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function main(args, { stdin = process.stdin, stdout = process.stdout, stderr = process.stderr, exit = process.exit } = {}) {
|
|
150
|
+
const agent = option(args, 'agent')
|
|
151
|
+
if (typeof agent !== 'string' || !ADAPTERS[agent]) {
|
|
152
|
+
stderr.write(`usage: bulkhead hook --agent <${AGENT_NAMES.join('|')}> (reads the agent's pre-tool payload on stdin)\n`)
|
|
153
|
+
return exit(1)
|
|
154
|
+
}
|
|
155
|
+
const { out, err, code } = await run(agent, await readStdin(stdin))
|
|
156
|
+
if (err) stderr.write(err.endsWith('\n') ? err : err + '\n')
|
|
157
|
+
// exit only once the pipe has taken the answer
|
|
158
|
+
if (out) stdout.write(out, () => exit(code))
|
|
159
|
+
else exit(code)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { main, run, judge, ADAPTERS, AGENT_NAMES, clineCommands }
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const os = require('os')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const { rejectArgs } = require('./cli-util')
|
|
6
|
+
|
|
7
|
+
// `bulkhead setup <agent> [--print|--write]`: the config that makes an agent call
|
|
8
|
+
// `bulkhead hook --agent <name>` before each shell command. --print (the default) shows it,
|
|
9
|
+
// --write merges it into the agent's config file after copying the original aside. Running it
|
|
10
|
+
// again changes nothing.
|
|
11
|
+
|
|
12
|
+
const SCRIPT = path.join(__dirname, '..', 'bin', 'bulkhead')
|
|
13
|
+
const MARK = 'bulkhead-managed'
|
|
14
|
+
|
|
15
|
+
const quote = s => (/[\s"]/.test(s) ? `"${s}"` : s)
|
|
16
|
+
// The node that runs this now, by absolute path: agents started from a desktop launcher often
|
|
17
|
+
// have no node on their PATH, and Copilot turns a hook that cannot start into a denial.
|
|
18
|
+
const launcher = (node = process.execPath, script = SCRIPT) => [node, script]
|
|
19
|
+
const commandLine = (agent, argv = launcher()) => [...argv.map(quote), 'hook', '--agent', agent].join(' ')
|
|
20
|
+
const isOurs = (text, agent) => new RegExp(`bulkhead\\\\?["']?\\s+hook\\s+--agent[ =]${agent}\\b`).test(text)
|
|
21
|
+
const pretty = v => JSON.stringify(v, null, 2) + '\n'
|
|
22
|
+
|
|
23
|
+
// The `command`/`bash` strings of every hook in a list of matcher groups.
|
|
24
|
+
const commandsIn = groups => JSON.stringify(groups || [])
|
|
25
|
+
|
|
26
|
+
// Adds `group` to doc[...path][event] unless one of our hooks is already there. Returns the doc.
|
|
27
|
+
function addGroup(doc, pathKeys, event, group, agent) {
|
|
28
|
+
let node = doc
|
|
29
|
+
for (const k of pathKeys) {
|
|
30
|
+
if (node[k] === undefined) node[k] = {}
|
|
31
|
+
if (!node[k] || typeof node[k] !== 'object' || Array.isArray(node[k])) throw new Error(`"${k}" is not an object`)
|
|
32
|
+
node = node[k]
|
|
33
|
+
}
|
|
34
|
+
if (node[event] === undefined) node[event] = []
|
|
35
|
+
if (!Array.isArray(node[event])) throw new Error(`"${event}" is not a list`)
|
|
36
|
+
if (!isOurs(commandsIn(node[event]), agent)) node[event].push(group)
|
|
37
|
+
return doc
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function jsonPlan(file, existing, build) {
|
|
41
|
+
const fragment = build({})
|
|
42
|
+
const snippet = pretty(fragment)
|
|
43
|
+
if (existing === null || !existing.trim()) return { file, snippet, next: snippet }
|
|
44
|
+
let doc
|
|
45
|
+
try {
|
|
46
|
+
doc = JSON.parse(existing)
|
|
47
|
+
} catch (e) {
|
|
48
|
+
return { file, snippet, error: `${file} is not plain JSON (${e.message}); add the snippet by hand` }
|
|
49
|
+
}
|
|
50
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return { file, snippet, error: `${file} does not hold a JSON object; add the snippet by hand` }
|
|
51
|
+
const before = pretty(doc)
|
|
52
|
+
let next
|
|
53
|
+
try {
|
|
54
|
+
next = pretty(build(doc))
|
|
55
|
+
} catch (e) {
|
|
56
|
+
return { file, snippet, error: `${file}: ${e.message}; add the snippet by hand` }
|
|
57
|
+
}
|
|
58
|
+
return { file, snippet, next: next === before ? null : next }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// A file bulkhead owns outright (a script, a plugin): install it, update ours, never replace another's.
|
|
62
|
+
function ownedPlan(file, existing, content, agent) {
|
|
63
|
+
if (existing !== null && !existing.includes(MARK) && !isOurs(existing, agent)) {
|
|
64
|
+
return { file, snippet: content, error: `${file} already exists and is not from bulkhead; put the call to \`bulkhead hook --agent ${agent}\` in it by hand` }
|
|
65
|
+
}
|
|
66
|
+
return { file, snippet: content, next: existing === content ? null : content, mode: 0o755 }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pluginSource = (agent, argv) => ({
|
|
70
|
+
opencode: `// ${MARK}: written by \`bulkhead setup opencode\`, safe to delete
|
|
71
|
+
import { spawnSync } from 'node:child_process'
|
|
72
|
+
|
|
73
|
+
const HOOK = ${JSON.stringify([...argv, 'hook', '--agent', 'opencode'])}
|
|
74
|
+
|
|
75
|
+
export const BulkheadPlugin = async ({ directory }) => ({
|
|
76
|
+
'tool.execute.before': async (input, output) => {
|
|
77
|
+
if (input.tool !== 'bash') return
|
|
78
|
+
const command = output.args && output.args.command
|
|
79
|
+
if (typeof command !== 'string' || !command) return
|
|
80
|
+
const cwd = (output.args && output.args.workdir) || directory
|
|
81
|
+
const r = spawnSync(HOOK[0], HOOK.slice(1), {
|
|
82
|
+
input: JSON.stringify({ command, cwd, session_id: input.sessionID }), encoding: 'utf8', timeout: 25000, windowsHide: true,
|
|
83
|
+
})
|
|
84
|
+
if (r.status === 2) throw new Error((r.stderr || 'blocked by bulkhead').trim())
|
|
85
|
+
},
|
|
86
|
+
})
|
|
87
|
+
`,
|
|
88
|
+
amp: `// ${MARK}: written by \`bulkhead setup amp\`, safe to delete
|
|
89
|
+
import { spawnSync } from 'node:child_process'
|
|
90
|
+
import type { PluginAPI } from '@ampcode/plugin'
|
|
91
|
+
|
|
92
|
+
const HOOK = ${JSON.stringify([...argv, 'hook', '--agent', 'amp'])}
|
|
93
|
+
|
|
94
|
+
export default function (amp: PluginAPI) {
|
|
95
|
+
amp.on('tool.call', async event => {
|
|
96
|
+
const shell = amp.helpers.shellCommandFromToolCall(event)
|
|
97
|
+
if (!shell || !shell.command) return { action: 'allow' }
|
|
98
|
+
const r = spawnSync(HOOK[0], HOOK.slice(1), {
|
|
99
|
+
input: JSON.stringify({ command: shell.command, cwd: shell.dir || process.cwd(), session_id: event.thread.id }),
|
|
100
|
+
encoding: 'utf8', timeout: 25000, windowsHide: true,
|
|
101
|
+
})
|
|
102
|
+
if (r.status === 2) return { action: 'reject-and-continue', message: (r.stderr || 'blocked by bulkhead').trim() }
|
|
103
|
+
return { action: 'allow' }
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
`,
|
|
107
|
+
})[agent]
|
|
108
|
+
|
|
109
|
+
const clineScript = (argv, platform) => platform === 'win32'
|
|
110
|
+
? `# ${MARK}: written by \`bulkhead setup cline\`\r\n$input | & ${argv.map(a => `"${a}"`).join(' ')} hook --agent cline\r\n`
|
|
111
|
+
: `#!/usr/bin/env bash\n# ${MARK}: written by \`bulkhead setup cline\`\nexec ${argv.map(a => `'${a.replace(/'/g, `'\\''`)}'`).join(' ')} hook --agent cline\n`
|
|
112
|
+
|
|
113
|
+
// agent: { where(home, env, platform) -> file, plan(file, existing, ctx) -> plan, note }
|
|
114
|
+
const SETUP = {
|
|
115
|
+
codex: {
|
|
116
|
+
where: (home, env) => path.join(env.CODEX_HOME || path.join(home, '.codex'), 'hooks.json'),
|
|
117
|
+
plan: (file, existing, { argv }) => jsonPlan(file, existing, doc => addGroup(doc, ['hooks'], 'PreToolUse',
|
|
118
|
+
{ matcher: '^Bash$', hooks: [{ type: 'command', command: commandLine('codex', argv), timeout: 30, statusMessage: 'bulkhead guards' }] }, 'codex')),
|
|
119
|
+
note: 'Codex asks you to review a new hook once: open /hooks in Codex and trust it.',
|
|
120
|
+
},
|
|
121
|
+
gemini: {
|
|
122
|
+
where: home => path.join(home, '.gemini', 'settings.json'),
|
|
123
|
+
plan: (file, existing, { argv }) => jsonPlan(file, existing, doc => addGroup(doc, ['hooks'], 'BeforeTool',
|
|
124
|
+
{ matcher: 'run_shell_command', hooks: [{ name: 'bulkhead', type: 'command', command: commandLine('gemini', argv), timeout: 30000 }] }, 'gemini')),
|
|
125
|
+
},
|
|
126
|
+
copilot: {
|
|
127
|
+
where: (home, env) => path.join(env.COPILOT_HOME || path.join(home, '.copilot'), 'hooks', 'bulkhead.json'),
|
|
128
|
+
plan: (file, existing, { argv }) => {
|
|
129
|
+
const command = commandLine('copilot', argv)
|
|
130
|
+
return ownedPlan(file, existing, pretty({ version: 1, hooks: { preToolUse: [{ type: 'command', bash: command, powershell: command, timeoutSec: 30 }] } }), 'copilot')
|
|
131
|
+
},
|
|
132
|
+
note: 'Copilot CLI treats a hook that fails to start as a denial. If you move or remove this node, run the setup again.',
|
|
133
|
+
},
|
|
134
|
+
cursor: {
|
|
135
|
+
where: home => path.join(home, '.cursor', 'hooks.json'),
|
|
136
|
+
plan: (file, existing, { argv }) => jsonPlan(file, existing, doc => {
|
|
137
|
+
doc.version = doc.version || 1
|
|
138
|
+
const list = (doc.hooks = doc.hooks || {}).beforeShellExecution = (doc.hooks.beforeShellExecution || [])
|
|
139
|
+
if (!isOurs(commandsIn(list), 'cursor')) list.push({ command: commandLine('cursor', argv), timeout: 30 })
|
|
140
|
+
return doc
|
|
141
|
+
}),
|
|
142
|
+
},
|
|
143
|
+
windsurf: {
|
|
144
|
+
where: home => path.join(home, '.codeium', 'windsurf', 'hooks.json'),
|
|
145
|
+
plan: (file, existing, { argv }) => jsonPlan(file, existing, doc => {
|
|
146
|
+
const list = (doc.hooks = doc.hooks || {}).pre_run_command = (doc.hooks.pre_run_command || [])
|
|
147
|
+
if (!isOurs(commandsIn(list), 'windsurf')) list.push({ command: commandLine('windsurf', argv), show_output: false })
|
|
148
|
+
return doc
|
|
149
|
+
}),
|
|
150
|
+
note: 'This is the Windsurf IDE user file. The JetBrains plugin reads ~/.codeium/hooks.json instead.',
|
|
151
|
+
},
|
|
152
|
+
droid: {
|
|
153
|
+
// hooks.json wins over the hooks key in settings.json, so a user who only has the second keeps it
|
|
154
|
+
where: home => {
|
|
155
|
+
const hooks = path.join(home, '.factory', 'hooks.json')
|
|
156
|
+
const settings = path.join(home, '.factory', 'settings.json')
|
|
157
|
+
try {
|
|
158
|
+
if (!fs.existsSync(hooks) && JSON.parse(fs.readFileSync(settings, 'utf8')).hooks) return settings
|
|
159
|
+
} catch {}
|
|
160
|
+
return hooks
|
|
161
|
+
},
|
|
162
|
+
plan: (file, existing, { argv }) => jsonPlan(file, existing, doc => addGroup(doc, path.basename(file) === 'settings.json' ? ['hooks'] : [], 'PreToolUse',
|
|
163
|
+
{ matcher: 'Execute', hooks: [{ type: 'command', command: commandLine('droid', argv), timeout: 30 }] }, 'droid')),
|
|
164
|
+
},
|
|
165
|
+
devin: {
|
|
166
|
+
where: (home, env, platform) => platform === 'win32'
|
|
167
|
+
? path.join(env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'devin', 'config.json')
|
|
168
|
+
: path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'devin', 'config.json'),
|
|
169
|
+
plan: (file, existing, { argv }) => jsonPlan(file, existing, doc => addGroup(doc, ['hooks'], 'PreToolUse',
|
|
170
|
+
{ matcher: 'exec', hooks: [{ type: 'command', command: commandLine('devin', argv) }] }, 'devin')),
|
|
171
|
+
},
|
|
172
|
+
cline: {
|
|
173
|
+
where: (home, env, platform) => path.join(env.CLINE_DIR || path.join(home, '.cline'), 'hooks', platform === 'win32' ? 'PreToolUse.ps1' : 'PreToolUse.sh'),
|
|
174
|
+
plan: (file, existing, { argv, platform }) => ownedPlan(file, existing, clineScript(argv, platform), 'cline'),
|
|
175
|
+
},
|
|
176
|
+
opencode: {
|
|
177
|
+
where: (home, env) => path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'opencode', 'plugins', 'bulkhead.js'),
|
|
178
|
+
plan: (file, existing, { argv }) => ownedPlan(file, existing, pluginSource('opencode', argv), 'opencode'),
|
|
179
|
+
note: 'Restart opencode to load the plugin.',
|
|
180
|
+
},
|
|
181
|
+
amp: {
|
|
182
|
+
where: (home, env) => path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'amp', 'plugins', 'bulkhead.ts'),
|
|
183
|
+
plan: (file, existing, { argv }) => ownedPlan(file, existing, pluginSource('amp', argv), 'amp'),
|
|
184
|
+
note: 'Run `plugins: reload` from the Amp command palette to load the plugin.',
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const AGENT_NAMES = Object.keys(SETUP)
|
|
189
|
+
|
|
190
|
+
function readIfExists(file) {
|
|
191
|
+
try { return fs.readFileSync(file, 'utf8') } catch (e) { if (e.code === 'ENOENT') return null; throw e }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// What `setup` would do for one agent. { file, snippet, next, error?, note }; next is null when
|
|
195
|
+
// the file already has it.
|
|
196
|
+
function plan(agent, { home = os.homedir(), env = process.env, platform = process.platform, argv = launcher() } = {}) {
|
|
197
|
+
const spec = SETUP[agent]
|
|
198
|
+
const file = spec.where(home, env, platform)
|
|
199
|
+
let existing
|
|
200
|
+
try { existing = readIfExists(file) } catch (e) { return { file, snippet: '', error: `cannot read ${file}: ${e.message}` } }
|
|
201
|
+
return { ...spec.plan(file, existing, { argv, platform }), existing, note: spec.note || null }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function backupName(file, exists = fs.existsSync, now = Date.now()) {
|
|
205
|
+
const base = file + '.bulkhead-backup'
|
|
206
|
+
return exists(base) ? `${base}-${now}` : base
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Writes the plan. Returns { wrote, backup }. The original is copied first and the new content
|
|
210
|
+
// goes in through a temporary file, so a crash never leaves a half-written config.
|
|
211
|
+
function apply(p) {
|
|
212
|
+
if (p.next === null || p.next === undefined) return { wrote: false, backup: null }
|
|
213
|
+
fs.mkdirSync(path.dirname(p.file), { recursive: true })
|
|
214
|
+
let backup = null
|
|
215
|
+
if (p.existing !== null && p.existing !== undefined) {
|
|
216
|
+
backup = backupName(p.file)
|
|
217
|
+
fs.copyFileSync(p.file, backup)
|
|
218
|
+
}
|
|
219
|
+
const tmp = `${p.file}.bulkhead-tmp-${process.pid}`
|
|
220
|
+
fs.writeFileSync(tmp, p.next, { mode: p.mode || 0o644 })
|
|
221
|
+
fs.renameSync(tmp, p.file)
|
|
222
|
+
return { wrote: true, backup }
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function main(args, { stdout = process.stdout, stderr = process.stderr, exit = process.exit, ctx = {} } = {}) {
|
|
226
|
+
const agent = args.find(a => !a.startsWith('--'))
|
|
227
|
+
const list = () => `agents: ${AGENT_NAMES.join(', ')}`
|
|
228
|
+
if (rejectArgs('setup', args.filter(a => a !== agent), { flags: ['print', 'write'], valued: [] }, stderr, null)) return exit(1)
|
|
229
|
+
if (!agent || !SETUP[agent]) {
|
|
230
|
+
stderr.write(`usage: bulkhead setup <agent> [--print|--write]\n${list()}\n`)
|
|
231
|
+
return exit(1)
|
|
232
|
+
}
|
|
233
|
+
if (args.includes('--print') && args.includes('--write')) {
|
|
234
|
+
stderr.write('bulkhead setup: use --print or --write, not both\n')
|
|
235
|
+
return exit(1)
|
|
236
|
+
}
|
|
237
|
+
const p = plan(agent, ctx)
|
|
238
|
+
if (p.error) {
|
|
239
|
+
stderr.write(`bulkhead setup: ${p.error}\n`)
|
|
240
|
+
if (p.snippet) stderr.write(`\n${p.snippet}`)
|
|
241
|
+
return exit(1)
|
|
242
|
+
}
|
|
243
|
+
if (!args.includes('--write')) {
|
|
244
|
+
stdout.write(`# ${p.file}${p.next === null ? ' (already set up)' : ''}\n${p.snippet}`)
|
|
245
|
+
if (p.note) stdout.write(`# ${p.note}\n`)
|
|
246
|
+
return exit(0)
|
|
247
|
+
}
|
|
248
|
+
const done = apply(p)
|
|
249
|
+
stdout.write(done.wrote
|
|
250
|
+
? `${p.existing === null ? 'created' : 'updated'} ${p.file}${done.backup ? `\nbackup of the original: ${done.backup}` : ''}\n`
|
|
251
|
+
: `${p.file} already has bulkhead; nothing changed\n`)
|
|
252
|
+
if (p.note) stdout.write(`${p.note}\n`)
|
|
253
|
+
return exit(0)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
module.exports = { main, plan, apply, backupName, commandLine, isOurs, SETUP, AGENT_NAMES }
|
package/src/agents.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const fs = require('fs')
|
|
3
|
+
const os = require('os')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
|
|
6
|
+
// The coding agents other than Claude Code that bulkhead recognizes as live sessions. Each entry
|
|
7
|
+
// matches the agent's CLI process. The patterns come from the published `bin` entries of the
|
|
8
|
+
// packages and the agents' install scripts (see docs/other-agents.md); an entry is only used for
|
|
9
|
+
// matching, so a pattern that misses just leaves that agent out of the session list.
|
|
10
|
+
const RUNTIME = /^(?:node|bun)(?:\.exe)?$/i
|
|
11
|
+
const nameIs = (p, re) => re.test(p.name || '') || re.test(basename(p.cmd))
|
|
12
|
+
|
|
13
|
+
function basename(cmd) {
|
|
14
|
+
const first = /^\s*(?:"([^"]+)"|'([^']+)'|(\S+))/.exec(cmd || '')
|
|
15
|
+
const prog = first ? first[1] || first[2] || first[3] : ''
|
|
16
|
+
return prog.split(/[\\/]/).pop()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// `node .../bin/<name>` or `node .../<name>.js`: a script launched through its runtime.
|
|
20
|
+
const script = base => new RegExp(`[\\\\/]${base}(?:\\.[cm]?js)?(?=["']?(?:\\s|$))`, 'i')
|
|
21
|
+
|
|
22
|
+
const AGENTS = {
|
|
23
|
+
codex: {
|
|
24
|
+
label: 'Codex',
|
|
25
|
+
// the npm package starts a native codex binary from node; the outer node process is the root
|
|
26
|
+
match: p => nameIs(p, /^codex(?:\.exe)?$/i) || (RUNTIME.test(p.name) && /@openai[\\/]codex[\\/]bin[\\/]codex\.js/.test(p.cmd)),
|
|
27
|
+
dirs: ['.codex'],
|
|
28
|
+
},
|
|
29
|
+
gemini: {
|
|
30
|
+
label: 'Gemini',
|
|
31
|
+
// gemini relaunches itself as a child with more heap; the outer process is the root
|
|
32
|
+
match: p => nameIs(p, /^gemini(?:\.exe)?$/i) || (RUNTIME.test(p.name) && (/@google[\\/]gemini-cli[\\/]/.test(p.cmd) || script('gemini').test(p.cmd))),
|
|
33
|
+
dirs: ['.gemini'],
|
|
34
|
+
},
|
|
35
|
+
copilot: {
|
|
36
|
+
label: 'Copilot',
|
|
37
|
+
match: p => nameIs(p, /^copilot(?:\.exe)?$/i) || (RUNTIME.test(p.name) && (/@github[\\/]copilot[\\/]/.test(p.cmd) || script('copilot').test(p.cmd))),
|
|
38
|
+
dirs: ['.copilot'],
|
|
39
|
+
},
|
|
40
|
+
cursor: {
|
|
41
|
+
label: 'Cursor',
|
|
42
|
+
// the cursor-agent CLI (also installed as `agent`); the Cursor editor itself is not matched
|
|
43
|
+
match: p => nameIs(p, /^cursor-agent(?:\.exe)?$/i) || /[\\/]cursor-agent[\\/]versions[\\/]/.test(p.cmd),
|
|
44
|
+
dirs: ['.cursor'],
|
|
45
|
+
},
|
|
46
|
+
opencode: {
|
|
47
|
+
label: 'opencode',
|
|
48
|
+
match: p => nameIs(p, /^opencode(?:\.exe)?$/i) || (RUNTIME.test(p.name) && script('opencode').test(p.cmd)),
|
|
49
|
+
dirs: ['.config/opencode', '.local/share/opencode'],
|
|
50
|
+
},
|
|
51
|
+
amp: {
|
|
52
|
+
label: 'Amp',
|
|
53
|
+
match: p => nameIs(p, /^amp(?:\.exe)?$/i) || /@ampcode[\\/]cli[\\/]/.test(p.cmd),
|
|
54
|
+
dirs: ['.config/amp'],
|
|
55
|
+
},
|
|
56
|
+
cline: {
|
|
57
|
+
label: 'Cline',
|
|
58
|
+
match: p => nameIs(p, /^cline(?:\.exe)?$/i) || (RUNTIME.test(p.name) && script('cline').test(p.cmd)),
|
|
59
|
+
dirs: ['.cline'],
|
|
60
|
+
},
|
|
61
|
+
droid: {
|
|
62
|
+
label: 'Droid',
|
|
63
|
+
match: p => nameIs(p, /^droid(?:\.exe)?$/i) || (RUNTIME.test(p.name) && script('droid').test(p.cmd)),
|
|
64
|
+
dirs: ['.factory'],
|
|
65
|
+
},
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Names of the agents other than Claude Code (and of the ones that only have a hook, no CLI).
|
|
69
|
+
const NAMES = Object.keys(AGENTS)
|
|
70
|
+
|
|
71
|
+
// The agent a process is the CLI of, or null. Claude Code is decided by sessions.isClaudeCli.
|
|
72
|
+
function agentOf(p) {
|
|
73
|
+
if (!p) return null
|
|
74
|
+
for (const name of NAMES) if (AGENTS[name].match(p)) return name
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function agentLabel(agent) {
|
|
79
|
+
return !agent || agent === 'claude' ? 'Claude' : (AGENTS[agent] && AGENTS[agent].label) || agent
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// A user who never installed another agent has none of its folders, so a process scan that only
|
|
83
|
+
// looks for other agents can be skipped without reading the process table.
|
|
84
|
+
function installedAgents(home = os.homedir(), exists = fs.existsSync, env = process.env) {
|
|
85
|
+
const xdg = env.XDG_CONFIG_HOME
|
|
86
|
+
return NAMES.filter(name => AGENTS[name].dirs.some(d => exists(path.join(home, d)) || (xdg && d.startsWith('.config/') && exists(path.join(xdg, d.slice(8))))))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { AGENTS, NAMES, agentOf, agentLabel, installedAgents }
|
package/src/allow-cli.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
const allowOnce = require('./allow-once')
|
|
3
|
+
const { rejectArgs, useColor, paint, table, shortPath } = require('./cli-util')
|
|
4
|
+
|
|
5
|
+
const USAGE = `usage: bulkhead allow <code>
|
|
6
|
+
bulkhead allow --list [--json]
|
|
7
|
+
|
|
8
|
+
Run this yourself, in your own terminal, when a guard blocked a command you want to go ahead.
|
|
9
|
+
<code> is the short code in the block message. The agent can then run that exact command, from
|
|
10
|
+
the same folder, once, within allowOnce.ttlMinutes (default 10). --list shows the codes waiting.
|
|
11
|
+
The agent cannot run this command: bulkhead blocks it.`
|
|
12
|
+
|
|
13
|
+
function ago(ms, now) {
|
|
14
|
+
const m = Math.max(0, Math.round((now - ms) / 60000))
|
|
15
|
+
return m < 1 ? 'just now' : `${m} min ago`
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function left(ms, now) {
|
|
19
|
+
return `${Math.max(1, Math.ceil((ms - now) / 60000))} min`
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function renderList(entries, { now = Date.now(), color = false } = {}) {
|
|
23
|
+
if (!entries.length) return 'no codes waiting. A code appears in the message when a guard blocks a command.'
|
|
24
|
+
const rows = [['CODE', 'STATE', 'GUARD', 'RULE', 'FOLDER', 'COMMAND', 'BLOCKED', 'LEFT']]
|
|
25
|
+
for (const e of entries) {
|
|
26
|
+
rows.push([e.code, e.approved ? 'allowed' : 'waiting', e.guard, e.rule, shortPath(e.cwd), e.cmd.slice(0, 50), ago(e.created, now), left(e.expires, now)])
|
|
27
|
+
}
|
|
28
|
+
const lines = table(rows)
|
|
29
|
+
return [paint(color, 'bold', lines[0]), ...lines.slice(1)].join('\n')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function main(args, { stdout = process.stdout, stderr = process.stderr, env = process.env, now = Date.now(), base } = {}) {
|
|
33
|
+
if (args.includes('--help') || args.includes('-h')) return stdout.write(USAGE + '\n')
|
|
34
|
+
if (args.includes('--list')) {
|
|
35
|
+
if (rejectArgs('allow', args, { flags: ['list', 'json'], valued: [] }, stderr, USAGE)) return
|
|
36
|
+
const entries = allowOnce.list({ base, now })
|
|
37
|
+
if (args.includes('--json')) return stdout.write(JSON.stringify(entries, null, 2) + '\n')
|
|
38
|
+
return stdout.write(renderList(entries, { now, color: useColor(stdout, env) }) + '\n')
|
|
39
|
+
}
|
|
40
|
+
const codes = args.filter(a => !a.startsWith('-'))
|
|
41
|
+
if (codes.length !== 1 || args.some(a => a.startsWith('-'))) {
|
|
42
|
+
stderr.write(USAGE + '\n')
|
|
43
|
+
process.exitCode = 1
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
const res = allowOnce.approve(codes[0], { base, now })
|
|
47
|
+
if (!res.ok) {
|
|
48
|
+
const why = {
|
|
49
|
+
unknown: `no waiting code ${codes[0].toUpperCase()}. It may have expired (codes last ${Math.round(allowOnce.ttlMs() / 60000)} minutes); run the blocked command again for a new one, or \`bulkhead allow --list\`.`,
|
|
50
|
+
used: `code ${codes[0].toUpperCase()} is already allowed and waiting for the agent to retry the command.`,
|
|
51
|
+
'no-code': 'give the code from the block message, for example: bulkhead allow 7F3K',
|
|
52
|
+
unwritable: 'could not write the allow state; check that the bulkhead state directory is writable.',
|
|
53
|
+
unreadable: 'could not read the allow state; check the bulkhead state directory.',
|
|
54
|
+
}[res.reason]
|
|
55
|
+
stderr.write(`bulkhead allow: ${why}\n`)
|
|
56
|
+
process.exitCode = 1
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
const e = res.entries[0]
|
|
60
|
+
const guards = res.entries.map(x => `${x.guard} ${x.rule}`).join(', ')
|
|
61
|
+
stdout.write(`Allowed once: ${e.cmd}\n folder ${e.cwd || '?'}\n guards ${guards}\n` +
|
|
62
|
+
`The agent can run exactly that command now, once, within ${left(e.expires, now)}.\n`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = { main, renderList, USAGE }
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
// How to let one blocked rule through for good: the exact config snippet and the
|
|
3
|
+
// BULKHEAD_DISABLE name. Shown in block messages (one line) and by `bulkhead explain`.
|
|
4
|
+
|
|
5
|
+
const RULE_KEYS = {
|
|
6
|
+
'rm-root': ['basics', 'rmRoot'],
|
|
7
|
+
'force-push-main': ['basics', 'forcePushMain'],
|
|
8
|
+
'protected-delete': ['basics', 'protectedDelete'],
|
|
9
|
+
'discard-changes': ['basics', 'discardChanges'],
|
|
10
|
+
'agent-author': ['basics', 'agentAuthor'],
|
|
11
|
+
'stage-all': ['checkoutGuard', 'stageAll'],
|
|
12
|
+
'remove-session-dir': ['checkoutGuard', 'removeSessionDir'],
|
|
13
|
+
'mass-kill': ['killGuard', 'massKill'],
|
|
14
|
+
'docker-mass': ['killGuard', 'dockerMass'],
|
|
15
|
+
'device-kill': ['killGuard', 'deviceKill'],
|
|
16
|
+
'app-quit': ['killGuard', 'appQuit'],
|
|
17
|
+
'service-stop': ['killGuard', 'serviceStop'],
|
|
18
|
+
'other-session-pid': ['killGuard', 'ownerCheck', 'owner-check'],
|
|
19
|
+
}
|
|
20
|
+
const SECTION = {
|
|
21
|
+
'kill-guard': 'killGuard', 'wait-guard': 'waitGuard', 'memory-guard': 'memoryGuard', 'port-guard': 'portGuard',
|
|
22
|
+
'checkout-guard': 'checkoutGuard', 'disk-guard': 'diskGuard', basics: 'basics', 'orphan-reaper': 'orphanReaper', 'secrets-guard': 'secretsGuard', 'edit-guard': 'editGuard',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// The shared program name a kill command aims at, to put in allowNames.
|
|
26
|
+
function killName(command) {
|
|
27
|
+
try {
|
|
28
|
+
const kill = require('./kill-guard')
|
|
29
|
+
const shell = require('./shell')
|
|
30
|
+
const names = kill.nameList()
|
|
31
|
+
const words = shell.unwrap(shell.prepare(String(command || ''), 'sh'), 'sh').split(/[^\w.*?-]+/)
|
|
32
|
+
return words.map(w => w.toLowerCase().replace(/\.exe$/, '')).find(w => w && kill.nameHits(w, names)) || null
|
|
33
|
+
} catch {
|
|
34
|
+
return null
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// { json, disable } for a guard and rule; json is null when the rule cannot be allowed.
|
|
39
|
+
function allowHint(guard, rule, { command = '', message = '' } = {}) {
|
|
40
|
+
const j = o => JSON.stringify(o)
|
|
41
|
+
if (rule === 'self-allow') return { json: null, disable: null }
|
|
42
|
+
const key = RULE_KEYS[rule]
|
|
43
|
+
if (key) return { json: j({ [key[0]]: { [key[1]]: false } }), disable: key[2] || rule }
|
|
44
|
+
const section = SECTION[guard]
|
|
45
|
+
if (!section) return { json: null, disable: guard }
|
|
46
|
+
const off = { json: j({ [section]: { enabled: false } }), disable: guard }
|
|
47
|
+
if (guard === 'kill-guard') return { json: j({ killGuard: { allowNames: [killName(command) || '<name>'] } }), disable: guard }
|
|
48
|
+
if (guard === 'port-guard') {
|
|
49
|
+
const port = /\bport (\d{2,5})\b/.exec(message)
|
|
50
|
+
return port ? { json: j({ portGuard: { ignorePorts: [Number(port[1])] } }), disable: guard } : off
|
|
51
|
+
}
|
|
52
|
+
if (guard === 'secrets-guard') {
|
|
53
|
+
const file = /\(([^()\n]+)\) to the model/.exec(message)
|
|
54
|
+
return { json: j({ secretsGuard: { allow: [file && !/\s/.test(file[1]) ? file[1] : '<file>'] } }), disable: guard }
|
|
55
|
+
}
|
|
56
|
+
if (guard === 'edit-guard') return { json: j({ editGuard: { mode: 'warn' } }), disable: guard }
|
|
57
|
+
if (guard === 'wait-guard') return { json: j({ waitGuard: { limitSeconds: 600 } }), disable: guard }
|
|
58
|
+
if (guard === 'memory-guard' && rule === 'low-memory') return { json: j({ memory: { minFreeGb: 1 } }), disable: guard }
|
|
59
|
+
return off
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { allowHint, killName }
|