@minionry/minion 0.7.36 → 0.7.39
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/bin/commands/minions.js +19 -0
- package/bin/minion.js +33 -4
- package/dist/server/cli/headless/claude-invoker-process.js +23 -7
- package/dist/server/cli/headless/claude-invoker-stream.js +8 -3
- package/dist/server/cli/headless/haiku-assessments.js +2 -1
- package/dist/server/cli/headless/mcp-config.js +2 -1
- package/dist/server/engines/claude/claude-command.js +1 -0
- package/dist/server/engines/factory.js +2 -0
- package/dist/server/engines/hermes/HermesEngine.js +112 -219
- package/dist/server/engines/hermes/hermes-gateway-registry.js +467 -0
- package/dist/server/index.js +10 -4
- package/dist/server/mcp/bouncer-integration.js +3 -2
- package/dist/server/mcp/classifier/ClaudeBouncerClassifier.js +35 -8
- package/dist/server/mcp/classifier/shadow-eval-scheduler.js +42 -0
- package/dist/server/mcp/classifier/telemetry.js +0 -0
- package/dist/server/mcp/permission-channels.js +176 -0
- package/dist/server/mcp/security-analysis.js +9 -1
- package/dist/server/mcp/security-audit.js +22 -2
- package/dist/server/mcp/security-patterns.js +73 -1
- package/dist/server/mcp/server.js +80 -50
- package/dist/server/routes/internal.js +6 -1
- package/dist/server/routes/notifications.js +6 -18
- package/dist/server/server-setup.js +24 -48
- package/dist/server/services/analytics.js +4 -2
- package/dist/server/services/browser/host-mock.js +6 -1
- package/dist/server/services/browser/host.js +27 -3
- package/dist/server/services/browser/reasoning-agent.js +3 -1
- package/dist/server/services/chain/chain-engine.js +494 -0
- package/dist/server/services/chain/chain-store.js +542 -0
- package/dist/server/services/git/haiku.js +2 -2
- package/dist/server/services/plan/agents/check-injection.md +43 -2
- package/dist/server/services/plan/agents/review-code.md +62 -3
- package/dist/server/services/plan/agents/review-quality.md +52 -2
- package/dist/server/services/plan/board-export.js +4 -0
- package/dist/server/services/plan/composer-prompt.js +27 -8
- package/dist/server/services/plan/composer.js +10 -4
- package/dist/server/services/plan/config-installer.js +4 -83
- package/dist/server/services/plan/executor.js +155 -73
- package/dist/server/services/plan/issue-effort.js +9 -21
- package/dist/server/services/plan/issue-prompt-builder.js +72 -9
- package/dist/server/services/plan/parser-core.js +6 -1
- package/dist/server/services/plan/quality-delta.js +6 -24
- package/dist/server/services/plan/readiness-planner.js +12 -4
- package/dist/server/services/plan/record-file.js +52 -0
- package/dist/server/services/plan/review-approval.js +64 -0
- package/dist/server/services/plan/review-gate.js +186 -77
- package/dist/server/services/plan/review-outcome.js +146 -0
- package/dist/server/services/plan/review-report.js +28 -1
- package/dist/server/services/plan/review-target.js +57 -0
- package/dist/server/services/plan/state-reconciler.js +3 -3
- package/dist/server/services/plan/template-diff.js +2 -0
- package/dist/server/services/plan/template-instantiator.js +4 -0
- package/dist/server/services/plan/watcher.js +11 -1
- package/dist/server/services/platform-reconnect.js +7 -0
- package/dist/server/services/platform-token-lifecycle.js +2 -1
- package/dist/server/services/platform.js +293 -50
- package/dist/server/services/relay-http-client.js +107 -0
- package/dist/server/services/schedule/agent-schedule-store.js +69 -8
- package/dist/server/services/schedule/agent-scheduler.js +48 -7
- package/dist/server/services/schedule/daily-timing.js +52 -0
- package/dist/server/services/schedule/prompt-file.js +55 -0
- package/dist/server/services/schedule/run-bootstrap.js +20 -5
- package/dist/server/services/schedule/schedule-store.js +54 -4
- package/dist/server/services/schedule/schedule-wire.js +17 -0
- package/dist/server/services/schedule/scheduler.js +43 -9
- package/dist/server/services/sdk/agent-schedule.js +294 -0
- package/dist/server/services/sdk/agents.js +95 -13
- package/dist/server/services/sdk/app-tools.js +44 -7
- package/dist/server/services/sdk/atomic-write.js +75 -0
- package/dist/server/services/sdk/browser.js +5 -1
- package/dist/server/services/sdk/composer.js +2 -0
- package/dist/server/services/sdk/file-change-notify.js +60 -13
- package/dist/server/services/sdk/files-app.js +12 -7
- package/dist/server/services/sdk/files-search.js +14 -0
- package/dist/server/services/sdk/files-transfer.js +1 -1
- package/dist/server/services/sdk/files-watch.js +721 -0
- package/dist/server/services/sdk/files-workspace.js +37 -11
- package/dist/server/services/sdk/inference-claude.js +4 -2
- package/dist/server/services/sdk/inference.js +16 -2
- package/dist/server/services/sdk/pm-chain.js +0 -0
- package/dist/server/services/sdk/pm-schedule.js +72 -15
- package/dist/server/services/sdk/pm.js +22 -0
- package/dist/server/services/sdk/rate-limits.js +32 -0
- package/dist/server/services/sdk/registry.js +40 -2
- package/dist/server/services/sdk/terminal.js +1 -1
- package/dist/server/services/sentry.js +5 -1
- package/dist/server/services/terminal/pty-manager.js +26 -11
- package/dist/server/services/terminal/pty-utils.js +12 -1
- package/dist/server/services/terminal/terminal-modes.js +87 -0
- package/dist/server/services/timeline/assembler.js +2 -2
- package/dist/server/services/timeline/index.js +1 -1
- package/dist/server/services/websocket/agent-schedule-handlers.js +62 -4
- package/dist/server/services/websocket/ask-user-question-bridge.js +2 -2
- package/dist/server/services/websocket/browser-agent-runtime.js +3 -1
- package/dist/server/services/websocket/browser-handlers.js +5 -2
- package/dist/server/services/websocket/browser-viewer-lifecycle.js +8 -0
- package/dist/server/services/websocket/file-explorer-handlers.js +17 -0
- package/dist/server/services/websocket/file-transfer-http.js +12 -6
- package/dist/server/services/websocket/handler.js +185 -33
- package/dist/server/services/websocket/msg-id-tracker.js +94 -19
- package/dist/server/services/websocket/plan-execution-handlers.js +33 -5
- package/dist/server/services/websocket/plan-handlers.js +10 -5
- package/dist/server/services/websocket/plan-helpers.js +23 -0
- package/dist/server/services/websocket/plan-issue-handlers.js +161 -4
- package/dist/server/services/websocket/plan-sprint-handlers.js +3 -2
- package/dist/server/services/websocket/schedule-handlers.js +8 -2
- package/dist/server/services/websocket/sdk-agent-host.js +5 -1
- package/dist/server/services/websocket/sdk-agent-schedule-host.js +30 -0
- package/dist/server/services/websocket/sdk-browser-host.js +11 -4
- package/dist/server/services/websocket/sdk-handlers.js +6 -1
- package/dist/server/services/websocket/sdk-pm-chain-host.js +20 -0
- package/dist/server/services/websocket/sdk-pm-execution-host.js +21 -14
- package/dist/server/services/websocket/sdk-pm-schedule-host.js +9 -0
- package/dist/server/services/websocket/sdk-terminal-host.js +7 -0
- package/dist/server/services/websocket/settings-handlers.js +6 -18
- package/dist/server/services/websocket/tab-broadcast.js +5 -1
- package/dist/server/services/websocket/terminal-handlers.js +51 -6
- package/dist/server/services/websocket/types.js +6 -5
- package/dist/server/services/websocket/viewer-interest.js +30 -0
- package/package.json +6 -6
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
|
+
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
export const BOUNCER_MCP_SERVER_NAME = 'minionry-bouncer';
|
|
5
|
+
export const PERMISSION_PROMPT_TOOL = 'approval_prompt';
|
|
6
|
+
export const PERMISSION_REQUEST_HOOK_TOOL = 'permission_request_hook';
|
|
7
|
+
export const PERMISSION_MODE_GUARD_TOOL = 'permission_mode_guard';
|
|
8
|
+
export const BOUNCER_PERMISSION_MODE = 'default';
|
|
9
|
+
export function bouncerToolName(tool) {
|
|
10
|
+
return `mcp__${BOUNCER_MCP_SERVER_NAME}__${tool}`;
|
|
11
|
+
}
|
|
12
|
+
export const BOUNCER_HOOK_TOOLS = [PERMISSION_REQUEST_HOOK_TOOL, PERMISSION_MODE_GUARD_TOOL].map(bouncerToolName);
|
|
13
|
+
const HOOK_INPUT_TEMPLATE = {
|
|
14
|
+
tool_name: '${tool_name}',
|
|
15
|
+
tool_input: '${tool_input}',
|
|
16
|
+
agent_id: '${agent_id}',
|
|
17
|
+
agent_type: '${agent_type}',
|
|
18
|
+
};
|
|
19
|
+
const BOUNCER_GOVERNED_MODES = new Set(['default', 'manual', 'plan', 'dontAsk']);
|
|
20
|
+
const MODE_GUARD_SKIPPED_TOOLS = [
|
|
21
|
+
'Read',
|
|
22
|
+
'Grep',
|
|
23
|
+
'Glob',
|
|
24
|
+
'ToolSearch',
|
|
25
|
+
'TodoWrite',
|
|
26
|
+
'TaskCreate',
|
|
27
|
+
'TaskGet',
|
|
28
|
+
'TaskList',
|
|
29
|
+
'TaskUpdate',
|
|
30
|
+
];
|
|
31
|
+
export const MODE_GUARD_MATCHER = `^(?!(${MODE_GUARD_SKIPPED_TOOLS.join('|')})$)`;
|
|
32
|
+
const MODE_GUARD_TIMEOUT_S = 30;
|
|
33
|
+
const MODE_GUARD_INPUT_TEMPLATE = {
|
|
34
|
+
tool_name: '${tool_name}',
|
|
35
|
+
agent_id: '${agent_id}',
|
|
36
|
+
agent_type: '${agent_type}',
|
|
37
|
+
permission_mode: '${permission_mode}',
|
|
38
|
+
};
|
|
39
|
+
export function bouncerSpawnSettings(permissionRequestTimeoutSeconds) {
|
|
40
|
+
return {
|
|
41
|
+
permissions: { disableBypassPermissionsMode: 'disable' },
|
|
42
|
+
hooks: {
|
|
43
|
+
PreToolUse: [
|
|
44
|
+
{
|
|
45
|
+
matcher: MODE_GUARD_MATCHER,
|
|
46
|
+
hooks: [
|
|
47
|
+
{
|
|
48
|
+
type: 'mcp_tool',
|
|
49
|
+
server: BOUNCER_MCP_SERVER_NAME,
|
|
50
|
+
tool: PERMISSION_MODE_GUARD_TOOL,
|
|
51
|
+
input: MODE_GUARD_INPUT_TEMPLATE,
|
|
52
|
+
timeout: MODE_GUARD_TIMEOUT_S,
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
PermissionRequest: [
|
|
58
|
+
{
|
|
59
|
+
matcher: '*',
|
|
60
|
+
hooks: [
|
|
61
|
+
{
|
|
62
|
+
type: 'mcp_tool',
|
|
63
|
+
server: BOUNCER_MCP_SERVER_NAME,
|
|
64
|
+
tool: PERMISSION_REQUEST_HOOK_TOOL,
|
|
65
|
+
input: HOOK_INPUT_TEMPLATE,
|
|
66
|
+
timeout: permissionRequestTimeoutSeconds,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function isRecord(value) {
|
|
75
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
76
|
+
}
|
|
77
|
+
function substitutedIdentifier(value) {
|
|
78
|
+
return typeof value === 'string' && value !== '' && !value.includes('${') ? value : undefined;
|
|
79
|
+
}
|
|
80
|
+
function toolInputObject(value) {
|
|
81
|
+
if (isRecord(value))
|
|
82
|
+
return value;
|
|
83
|
+
if (typeof value !== 'string')
|
|
84
|
+
return undefined;
|
|
85
|
+
try {
|
|
86
|
+
const parsed = JSON.parse(value);
|
|
87
|
+
return isRecord(parsed) ? parsed : undefined;
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export function parsePermissionRequestHookArgs(args) {
|
|
94
|
+
if (!isRecord(args))
|
|
95
|
+
return { kind: 'malformed', reason: 'arguments are not an object' };
|
|
96
|
+
const subagentId = substitutedIdentifier(args.agent_id);
|
|
97
|
+
if (!subagentId)
|
|
98
|
+
return { kind: 'main-agent' };
|
|
99
|
+
const toolName = substitutedIdentifier(args.tool_name);
|
|
100
|
+
if (!toolName)
|
|
101
|
+
return { kind: 'malformed', reason: 'tool_name is missing' };
|
|
102
|
+
const toolInput = toolInputObject(args.tool_input);
|
|
103
|
+
if (!toolInput)
|
|
104
|
+
return { kind: 'malformed', reason: 'tool_input is not a JSON object' };
|
|
105
|
+
const subagentType = substitutedIdentifier(args.agent_type);
|
|
106
|
+
return {
|
|
107
|
+
kind: 'subagent',
|
|
108
|
+
toolName,
|
|
109
|
+
toolInput,
|
|
110
|
+
requester: { subagentId, ...(subagentType ? { subagentType } : {}) },
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function toPermissionPromptResponse(verdict, input) {
|
|
114
|
+
if (verdict.behavior === 'deny')
|
|
115
|
+
return { behavior: 'deny', message: verdict.message };
|
|
116
|
+
return { behavior: 'allow', updatedInput: verdict.updatedInput ?? input, message: verdict.caution };
|
|
117
|
+
}
|
|
118
|
+
export function toPermissionRequestHookOutput(verdict) {
|
|
119
|
+
const decision = verdict.behavior === 'deny'
|
|
120
|
+
? { behavior: 'deny', message: verdict.message }
|
|
121
|
+
: { behavior: 'allow', ...(verdict.updatedInput ? { updatedInput: verdict.updatedInput } : {}) };
|
|
122
|
+
return { hookSpecificOutput: { hookEventName: 'PermissionRequest', decision } };
|
|
123
|
+
}
|
|
124
|
+
export const NO_HOOK_DECISION = Object.freeze({});
|
|
125
|
+
export function decidePermissionModeGuard(args) {
|
|
126
|
+
if (!isRecord(args))
|
|
127
|
+
return { verdict: 'none' };
|
|
128
|
+
const permissionMode = substitutedIdentifier(args.permission_mode);
|
|
129
|
+
if (!permissionMode)
|
|
130
|
+
return { verdict: 'none' };
|
|
131
|
+
const toolName = substitutedIdentifier(args.tool_name);
|
|
132
|
+
if (toolName && MODE_GUARD_SKIPPED_TOOLS.includes(toolName))
|
|
133
|
+
return { verdict: 'none', permissionMode };
|
|
134
|
+
return { verdict: BOUNCER_GOVERNED_MODES.has(permissionMode) ? 'none' : 'ask', permissionMode };
|
|
135
|
+
}
|
|
136
|
+
export function toPermissionModeGuardOutput(decision) {
|
|
137
|
+
if (decision.verdict === 'none')
|
|
138
|
+
return NO_HOOK_DECISION;
|
|
139
|
+
return {
|
|
140
|
+
hookSpecificOutput: {
|
|
141
|
+
hookEventName: 'PreToolUse',
|
|
142
|
+
permissionDecision: 'ask',
|
|
143
|
+
permissionDecisionReason: `Minionry sends this call to its Security Bouncer: the ${decision.permissionMode} permission mode would otherwise approve it without review.`,
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
function canonicalJson(value) {
|
|
148
|
+
return JSON.stringify(value ?? null, (_key, nested) => isRecord(nested) ? Object.fromEntries(Object.keys(nested).sort().map((key) => [key, nested[key]])) : nested);
|
|
149
|
+
}
|
|
150
|
+
export function permissionDecisionKey(toolName, input) {
|
|
151
|
+
return createHash('sha256').update(toolName).update('\0').update(canonicalJson(input)).digest('hex');
|
|
152
|
+
}
|
|
153
|
+
export const PERMISSION_JOIN_WINDOW_MS = 25;
|
|
154
|
+
export class PermissionDecisionCoalescer {
|
|
155
|
+
joinWindow;
|
|
156
|
+
inFlight = new Map();
|
|
157
|
+
constructor(joinWindow = () => new Promise((resolve) => setTimeout(resolve, PERMISSION_JOIN_WINDOW_MS))) {
|
|
158
|
+
this.joinWindow = joinWindow;
|
|
159
|
+
}
|
|
160
|
+
decide(key, requester, review) {
|
|
161
|
+
const joined = this.inFlight.get(key);
|
|
162
|
+
if (joined) {
|
|
163
|
+
joined.requester ??= requester;
|
|
164
|
+
return joined.decision;
|
|
165
|
+
}
|
|
166
|
+
const entry = { requester };
|
|
167
|
+
entry.decision = this.joinWindow()
|
|
168
|
+
.then(() => review(entry.requester))
|
|
169
|
+
.finally(() => {
|
|
170
|
+
if (this.inFlight.get(key) === entry)
|
|
171
|
+
this.inFlight.delete(key);
|
|
172
|
+
});
|
|
173
|
+
this.inFlight.set(key, entry);
|
|
174
|
+
return entry.decision;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -175,7 +175,7 @@ const READ_ONLY_DISQUALIFIERS = [
|
|
|
175
175
|
/^(env|nice|timeout|nohup|stdbuf|ionice|setsid|chroot|doas)(\s|$)/i,
|
|
176
176
|
];
|
|
177
177
|
const SENSITIVE_READ_TARGETS = /(\/etc\/(shadow|passwd|sudoers)|\.ssh\/|\.aws\/|\.gnupg\/|\.docker\/config|\.kube\/config|\.git-credentials|\.npmrc|\.netrc|\.pypirc|authorized_keys|id_rsa|id_ed25519|id_ecdsa|\.env(\.|\b)|credentials|\.pem\b|\.p12\b|\.keystore\b)/i;
|
|
178
|
-
const CREDENTIAL_FILE_PATHS = /(\/etc\/(?:shadow|sudoers)\b|[~/]\.ssh
|
|
178
|
+
const CREDENTIAL_FILE_PATHS = /(\/etc\/(?:shadow|sudoers)\b|[~/]\.ssh(?![\w-])|[~/]\.aws(?![\w-])|[~/]\.gnupg(?![\w-])|[~/]\.docker\/config|[~/]\.kube\/config|\.git-credentials\b|\.netrc\b|\.pypirc\b|authorized_keys\b|\bid_(?:rsa|ed25519|ecdsa|dsa)\b|\.pem\b|\.p12\b|\.pfx\b|\.keystore\b|\.config\/(?:google-chrome|chromium|BraveSoftware|microsoft-edge)(?![\w-])|Application Support\/(?:Google\/Chrome|Chromium|BraveSoftware|Microsoft Edge|Firefox)(?![\w-])|[~/]\.mozilla\/firefox(?![\w-])|\bLogin Data\b|\bcookies\.sqlite\b)/i;
|
|
179
179
|
function segmentIsReadOnly(rawSegment) {
|
|
180
180
|
const segment = unwrapCommandPrefix(rawSegment.trim()).replace(/^\[\[?\s/, 'test ');
|
|
181
181
|
if (READ_ONLY_DISQUALIFIERS.some((p) => p.test(segment)))
|
|
@@ -304,6 +304,8 @@ export function requiresAIReview(operation) {
|
|
|
304
304
|
const op = normalizeOperation(operation);
|
|
305
305
|
if (matchesPattern(op, SENSITIVE_PATHS))
|
|
306
306
|
return true;
|
|
307
|
+
if (namesCredentialStore(op))
|
|
308
|
+
return true;
|
|
307
309
|
if (/^Bash:/i.test(op) && containsAnyExpansion(op) && !bashExpansionIsReadOnlySafe(op)) {
|
|
308
310
|
return true;
|
|
309
311
|
}
|
|
@@ -330,6 +332,12 @@ export function requiresAIReview(operation) {
|
|
|
330
332
|
export function isSensitivePath(operation) {
|
|
331
333
|
return matchesPattern(operation, SENSITIVE_PATHS);
|
|
332
334
|
}
|
|
335
|
+
export function namesCredentialStore(operation) {
|
|
336
|
+
const op = normalizeOperation(operation);
|
|
337
|
+
if (!/^Bash:/i.test(op))
|
|
338
|
+
return false;
|
|
339
|
+
return CREDENTIAL_FILE_PATHS.test(op);
|
|
340
|
+
}
|
|
333
341
|
export function classifyRisk(operation) {
|
|
334
342
|
const criticalThreat = matchesPattern(operation, CRITICAL_THREATS);
|
|
335
343
|
if (criticalThreat) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, } from 'node:fs';
|
|
4
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, readSync, statSync, } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { SPACE_DIRNAME, SPACE_LAYOUT } from '../services/space-layout.js';
|
|
7
7
|
const DEFAULT_LOG_SUBDIR = join(SPACE_DIRNAME, SPACE_LAYOUT.logs);
|
|
@@ -46,7 +46,7 @@ export class SecurityAuditLogger {
|
|
|
46
46
|
return [];
|
|
47
47
|
let content;
|
|
48
48
|
try {
|
|
49
|
-
content =
|
|
49
|
+
content = this.readTail(opts?.limit);
|
|
50
50
|
}
|
|
51
51
|
catch (error) {
|
|
52
52
|
console.error('[SecurityAudit] Failed to read log:', error);
|
|
@@ -64,6 +64,26 @@ export class SecurityAuditLogger {
|
|
|
64
64
|
}
|
|
65
65
|
return entries;
|
|
66
66
|
}
|
|
67
|
+
readTail(limit) {
|
|
68
|
+
if (!limit || limit <= 0)
|
|
69
|
+
return readFileSync(this.logFile, 'utf-8');
|
|
70
|
+
const BYTES_PER_ENTRY_ESTIMATE = 2048;
|
|
71
|
+
const want = limit * BYTES_PER_ENTRY_ESTIMATE;
|
|
72
|
+
const { size } = statSync(this.logFile);
|
|
73
|
+
if (size <= want)
|
|
74
|
+
return readFileSync(this.logFile, 'utf-8');
|
|
75
|
+
const fd = openSync(this.logFile, 'r');
|
|
76
|
+
try {
|
|
77
|
+
const buf = Buffer.allocUnsafe(want);
|
|
78
|
+
readSync(fd, buf, 0, want, size - want);
|
|
79
|
+
const text = buf.toString('utf-8');
|
|
80
|
+
const firstBreak = text.indexOf('\n');
|
|
81
|
+
return firstBreak === -1 ? '' : text.slice(firstBreak + 1);
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
closeSync(fd);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
67
87
|
logDecision(operation, decision, confidence, reasoning, metadata) {
|
|
68
88
|
this.log({
|
|
69
89
|
timestamp: new Date().toISOString(),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { resolve } from 'node:path';
|
|
4
|
-
export { classifyRisk, isDeployMode, isSensitivePath, requiresAIReview } from './security-analysis.js';
|
|
4
|
+
export { classifyRisk, isDeployMode, isSensitivePath, namesCredentialStore, requiresAIReview, } from './security-analysis.js';
|
|
5
5
|
export const SENSITIVE_PATHS = [
|
|
6
6
|
{ pattern: /^(Write|Edit):\s*\/etc\//i, reason: 'System configuration - verify user intent' },
|
|
7
7
|
{ pattern: /^(Write|Edit):\s*\/(bin|sbin|usr\/bin|usr\/sbin|usr\/local\/bin|usr\/local\/sbin)\//i, reason: 'System binaries - verify user intent' },
|
|
@@ -181,6 +181,78 @@ export const NEEDS_AI_REVIEW = [
|
|
|
181
181
|
reason: 'Inline script code with dynamic execution or system access - verify user intent'
|
|
182
182
|
},
|
|
183
183
|
{ pattern: /\bclaude\b.*\bmcp\b.*\badd\b/i, reason: 'Adding MCP server - verify source is trusted' },
|
|
184
|
+
{
|
|
185
|
+
pattern: /(?:^|[\s;|&`("'])history\s+-c\b/i,
|
|
186
|
+
reason: 'Clearing shell history - potential cover-up of a prior operation'
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
pattern: /(?:^|[\s;|&`("'])(?:rm|shred|truncate|unlink)\b[^\n;|&]*[~/.](?:bash_history|zsh_history|sh_history|python_history)\b/i,
|
|
190
|
+
reason: 'Destroying shell history - potential cover-up of a prior operation'
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
pattern: /(?:(?:^|[\s;|&`("'])(?:truncate|shred|rm)\b[^\n;|&]*|>\s*)\/var\/log\//i,
|
|
194
|
+
reason: 'Truncating or deleting a system log - potential audit-trail tampering'
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
pattern: /\bsystemctl\s+(?:stop|disable|mask)\s+\S*(?:auditd|rsyslog|syslog|journald|falco|osquery|wazuh)\S*/i,
|
|
198
|
+
reason: 'Stopping an audit/monitoring service - potential oversight disabling'
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
pattern: /\bservice\s+\S*(?:auditd|rsyslog|syslog|falco|osquery|wazuh)\S*\s+(?:stop|disable)\b/i,
|
|
202
|
+
reason: 'Stopping an audit/monitoring service - potential oversight disabling'
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
pattern: /(?:^|[\s;|&`("'])(?:auditctl\s+-[eD]\b|launchctl\s+(?:unload|bootout|disable)\b)/i,
|
|
206
|
+
reason: 'Disabling audit or launch services - potential oversight disabling'
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
pattern: /(?:^|[\s;|&`("'/])(?:ngrok|cloudflared|localtunnel|bore|frpc|pagekite)\b/i,
|
|
210
|
+
reason: 'Dev tunnel - exposes a local service to the public internet; verify user intent'
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
pattern: /(?:^|[\s;|&`("'/])tailscale\s+funnel\b/i,
|
|
214
|
+
reason: 'Tailscale funnel - exposes a local service publicly; verify user intent'
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
pattern: /\bssh\b[^\n;|&]*\s-R\s*\S/i,
|
|
218
|
+
reason: 'SSH remote port forward - exposes a local port to a remote host'
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
pattern: /(?:^|[\s;|&`("'/])socat\b[^\n]*(?:EXEC|SYSTEM):/i,
|
|
222
|
+
reason: 'socat with EXEC/SYSTEM - bind or reverse shell'
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
pattern: /(?:^|[\s;|&`("'/])socat\b[^\n]*TCP\d?-LISTEN/i,
|
|
226
|
+
reason: 'socat listener - potential bind shell'
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
pattern: /\b(?:npm|yarn|pnpm|bun)\s+config\s+set\s+(?:@\S+:)?registry\b/i,
|
|
230
|
+
reason: 'Persisting a non-default npm registry - potential supply chain redirection'
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
pattern: /\bpip3?\s+config\s+set\b[^\n]*index-url\b/i,
|
|
234
|
+
reason: 'Persisting a non-default pip index - potential supply chain redirection'
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
pattern: /\bgit\s+push\b[^\n;|&]*\s(?:--force(?!-with-lease)|-f)(?=\s|$)/i,
|
|
238
|
+
reason: 'Force-push overwrites remote history - verify the branch and user intent'
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
pattern: /\bkubectl\s+delete\s+(?:ns|namespace|deployment|statefulset|pvc|persistentvolumeclaim|secret)\b/i,
|
|
242
|
+
reason: 'Kubernetes resource teardown - verify the target matches user intent'
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
pattern: /\b(?:terraform|tofu)\s+destroy\b/i,
|
|
246
|
+
reason: 'Terraform destroy - tears down managed infrastructure; verify user intent'
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
pattern: /\baws\s+s3\s+(?:rb|rm)\b[^\n]*(?:--force|--recursive)\b/i,
|
|
250
|
+
reason: 'Recursive S3 bucket deletion - verify the target matches user intent'
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
pattern: /\baws\s+(?:ec2\s+terminate-instances|rds\s+delete-db-(?:instance|cluster))\b/i,
|
|
254
|
+
reason: 'Cloud instance/database teardown - verify the target matches user intent'
|
|
255
|
+
},
|
|
184
256
|
...GIT_SDK_REMOTE_REVIEW_TOOLS,
|
|
185
257
|
];
|
|
186
258
|
export const BROWSER_SAFE_TOOLS = [
|
|
@@ -4,8 +4,9 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
5
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
6
6
|
import { Agent } from 'undici';
|
|
7
|
-
import { reviewEnginePermission, sdkRunReviewContextFromEnv, setRemoteDecisionCache, } from './bouncer-integration.js';
|
|
7
|
+
import { formatDenialMessage, reviewEnginePermission, sdkRunReviewContextFromEnv, setRemoteDecisionCache, } from './bouncer-integration.js';
|
|
8
8
|
import { sweepClassifierTranscripts } from './classifier/ClaudeBouncerClassifier.js';
|
|
9
|
+
import { BOUNCER_MCP_SERVER_NAME, decidePermissionModeGuard, NO_HOOK_DECISION, PERMISSION_MODE_GUARD_TOOL, PERMISSION_PROMPT_TOOL, PERMISSION_REQUEST_HOOK_TOOL, PermissionDecisionCoalescer, parsePermissionRequestHookArgs, permissionDecisionKey, toPermissionModeGuardOutput, toPermissionPromptResponse, toPermissionRequestHookOutput, } from './permission-channels.js';
|
|
9
10
|
const REMOTE_CACHE_TIMEOUT_MS = 400;
|
|
10
11
|
function installRemoteDecisionCache() {
|
|
11
12
|
const port = process.env.MINIONRY_PORT;
|
|
@@ -42,7 +43,7 @@ function installRemoteDecisionCache() {
|
|
|
42
43
|
}
|
|
43
44
|
const askUserQuestionDispatcher = new Agent({ headersTimeout: 0, bodyTimeout: 0 });
|
|
44
45
|
const server = new Server({
|
|
45
|
-
name:
|
|
46
|
+
name: BOUNCER_MCP_SERVER_NAME,
|
|
46
47
|
version: '1.0.0',
|
|
47
48
|
}, {
|
|
48
49
|
capabilities: {
|
|
@@ -53,7 +54,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
53
54
|
return {
|
|
54
55
|
tools: [
|
|
55
56
|
{
|
|
56
|
-
name:
|
|
57
|
+
name: PERMISSION_PROMPT_TOOL,
|
|
57
58
|
description: 'Analyze and approve/deny tool use requests from Claude Code. Integrates with Minionry security bouncer for AI-powered risk analysis.',
|
|
58
59
|
inputSchema: {
|
|
59
60
|
type: 'object',
|
|
@@ -70,6 +71,32 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
|
70
71
|
required: ['tool_name', 'input'],
|
|
71
72
|
},
|
|
72
73
|
},
|
|
74
|
+
{
|
|
75
|
+
name: PERMISSION_REQUEST_HOOK_TOOL,
|
|
76
|
+
description: 'Internal: answers Claude Code PermissionRequest hooks for subagent tool calls. Called by the hook, never by the model.',
|
|
77
|
+
inputSchema: {
|
|
78
|
+
type: 'object',
|
|
79
|
+
properties: {
|
|
80
|
+
tool_name: { type: 'string', description: 'Name of the tool being requested' },
|
|
81
|
+
tool_input: { description: 'Tool input parameters, JSON-encoded by the hook' },
|
|
82
|
+
agent_id: { type: 'string', description: 'The requesting subagent; empty for the main agent' },
|
|
83
|
+
agent_type: { type: 'string', description: 'The requesting subagent type' },
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: PERMISSION_MODE_GUARD_TOOL,
|
|
89
|
+
description: 'Internal: answers Claude Code PreToolUse hooks so no permission mode approves a call without the Bouncer. Called by the hook, never by the model.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
tool_name: { type: 'string', description: 'Name of the tool about to run' },
|
|
94
|
+
agent_id: { type: 'string', description: 'The calling subagent; empty for the main agent' },
|
|
95
|
+
agent_type: { type: 'string', description: 'The calling subagent type' },
|
|
96
|
+
permission_mode: { type: 'string', description: 'The permission mode the call runs in' },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
73
100
|
],
|
|
74
101
|
};
|
|
75
102
|
});
|
|
@@ -81,7 +108,7 @@ async function bridgeAskUserQuestion(input) {
|
|
|
81
108
|
const toolUseId = process.env.MINIONRY_CURRENT_TOOL_USE_ID || `aq-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
82
109
|
if (!port || !tabId || !secret || !capabilityToken) {
|
|
83
110
|
console.error('[MCP Bouncer] AskUserQuestion: missing routing context (port/tabId/secret/capability token) — passing through with no answers');
|
|
84
|
-
return { behavior: 'allow'
|
|
111
|
+
return { behavior: 'allow' };
|
|
85
112
|
}
|
|
86
113
|
try {
|
|
87
114
|
const requestOptions = {
|
|
@@ -97,7 +124,7 @@ async function bridgeAskUserQuestion(input) {
|
|
|
97
124
|
const res = await fetch(`http://127.0.0.1:${port}/internal/ask-user-question`, requestOptions);
|
|
98
125
|
if (!res.ok) {
|
|
99
126
|
console.error(`[MCP Bouncer] AskUserQuestion bridge returned ${res.status} — passing through with no answers`);
|
|
100
|
-
return { behavior: 'allow'
|
|
127
|
+
return { behavior: 'allow' };
|
|
101
128
|
}
|
|
102
129
|
const json = (await res.json());
|
|
103
130
|
const answers = json.answers && typeof json.answers === 'object' ? json.answers : {};
|
|
@@ -108,76 +135,79 @@ async function bridgeAskUserQuestion(input) {
|
|
|
108
135
|
}
|
|
109
136
|
catch (err) {
|
|
110
137
|
console.error(`[MCP Bouncer] AskUserQuestion bridge failed: ${err instanceof Error ? err.message : String(err)} — passing through with no answers`);
|
|
111
|
-
return { behavior: 'allow'
|
|
138
|
+
return { behavior: 'allow' };
|
|
112
139
|
}
|
|
113
140
|
}
|
|
114
|
-
function
|
|
141
|
+
function verdictFromDecision(decision) {
|
|
115
142
|
if (decision.decision === 'deny') {
|
|
116
|
-
return {
|
|
117
|
-
behavior: 'deny',
|
|
118
|
-
message: `🚫 ${decision.reasoning}${decision.alternative ? `\n\nAlternative: ${decision.alternative}` : ''}`,
|
|
119
|
-
};
|
|
143
|
+
return { behavior: 'deny', message: formatDenialMessage(decision) };
|
|
120
144
|
}
|
|
121
|
-
return
|
|
122
|
-
behavior: 'allow',
|
|
123
|
-
|
|
124
|
-
message: decision.decision === 'warn_allow'
|
|
125
|
-
? `⚠️ Allowed with caution: ${decision.reasoning}`
|
|
126
|
-
: undefined,
|
|
127
|
-
};
|
|
145
|
+
return decision.decision === 'warn_allow'
|
|
146
|
+
? { behavior: 'allow', caution: `⚠️ Allowed with caution: ${decision.reasoning}` }
|
|
147
|
+
: { behavior: 'allow' };
|
|
128
148
|
}
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
132
|
-
}
|
|
133
|
-
const { tool_name, input } = request.params.arguments;
|
|
134
|
-
if (tool_name === 'AskUserQuestion') {
|
|
149
|
+
async function reviewPermission(toolName, input, requester) {
|
|
150
|
+
if (toolName === 'AskUserQuestion') {
|
|
135
151
|
console.error('[MCP Bouncer] AskUserQuestion received — bridging to web UI');
|
|
136
|
-
|
|
137
|
-
return {
|
|
138
|
-
content: [{ type: 'text', text: JSON.stringify(response) }],
|
|
139
|
-
};
|
|
152
|
+
return bridgeAskUserQuestion(input);
|
|
140
153
|
}
|
|
141
|
-
|
|
154
|
+
const from = requester ? ` from subagent ${requester.subagentType ?? '(unknown type)'} ${requester.subagentId}` : '';
|
|
155
|
+
console.error(`[MCP Bouncer] Analyzing ${toolName} request${from}...`);
|
|
142
156
|
const sdkRunContext = sdkRunReviewContextFromEnv(process.env);
|
|
143
157
|
try {
|
|
144
158
|
const decision = await reviewEnginePermission({
|
|
145
|
-
toolName
|
|
159
|
+
toolName,
|
|
146
160
|
input,
|
|
147
161
|
context: {
|
|
148
162
|
purpose: `Tool use request from Claude`,
|
|
149
163
|
workingDirectory: process.cwd(),
|
|
150
164
|
userRequest: process.env.BOUNCER_USER_PROMPT,
|
|
151
165
|
...(sdkRunContext ?? {}),
|
|
166
|
+
...(requester ?? {}),
|
|
152
167
|
},
|
|
153
168
|
});
|
|
154
169
|
console.error(`[MCP Bouncer] Decision: ${decision.decision} (${decision.confidence}% confidence)`);
|
|
155
170
|
console.error(`[MCP Bouncer] Reasoning: ${decision.reasoning}`);
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
content: [
|
|
159
|
-
{
|
|
160
|
-
type: 'text',
|
|
161
|
-
text: JSON.stringify(response),
|
|
162
|
-
},
|
|
163
|
-
],
|
|
164
|
-
};
|
|
171
|
+
return verdictFromDecision(decision);
|
|
165
172
|
}
|
|
166
173
|
catch (error) {
|
|
167
174
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
168
175
|
console.error(`[MCP Bouncer] Error: ${errorMessage}`);
|
|
169
|
-
return {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
176
|
+
return { behavior: 'deny', message: `Security analysis failed: ${errorMessage}. Denying for safety.` };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const permissionDecisions = new PermissionDecisionCoalescer();
|
|
180
|
+
function decidePermission(toolName, input, requester) {
|
|
181
|
+
return permissionDecisions.decide(permissionDecisionKey(toolName, input), requester, (who) => reviewPermission(toolName, input, who));
|
|
182
|
+
}
|
|
183
|
+
function textResult(value) {
|
|
184
|
+
return { content: [{ type: 'text', text: JSON.stringify(value) }] };
|
|
185
|
+
}
|
|
186
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
187
|
+
if (request.params.name === PERMISSION_PROMPT_TOOL) {
|
|
188
|
+
const { tool_name, input } = request.params.arguments;
|
|
189
|
+
const verdict = await decidePermission(tool_name, input);
|
|
190
|
+
return textResult(toPermissionPromptResponse(verdict, input));
|
|
191
|
+
}
|
|
192
|
+
if (request.params.name === PERMISSION_REQUEST_HOOK_TOOL) {
|
|
193
|
+
const call = parsePermissionRequestHookArgs(request.params.arguments);
|
|
194
|
+
if (call.kind === 'malformed') {
|
|
195
|
+
console.error(`[MCP Bouncer] PermissionRequest hook: ${call.reason} — no decision`);
|
|
196
|
+
}
|
|
197
|
+
if (call.kind !== 'subagent')
|
|
198
|
+
return textResult(NO_HOOK_DECISION);
|
|
199
|
+
const verdict = await decidePermission(call.toolName, call.toolInput, call.requester);
|
|
200
|
+
return textResult(toPermissionRequestHookOutput(verdict));
|
|
201
|
+
}
|
|
202
|
+
if (request.params.name === PERMISSION_MODE_GUARD_TOOL) {
|
|
203
|
+
const decision = decidePermissionModeGuard(request.params.arguments);
|
|
204
|
+
if (decision.verdict === 'ask') {
|
|
205
|
+
const args = request.params.arguments;
|
|
206
|
+
console.error(`[MCP Bouncer] Mode guard: ${String(args?.tool_name)} runs in ${decision.permissionMode} (${String(args?.agent_type || 'main agent')}) — asking the Bouncer`);
|
|
207
|
+
}
|
|
208
|
+
return textResult(toPermissionModeGuardOutput(decision));
|
|
180
209
|
}
|
|
210
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
181
211
|
});
|
|
182
212
|
async function main() {
|
|
183
213
|
installRemoteDecisionCache();
|
|
@@ -254,7 +254,8 @@ const BROWSER_OP_HANDLERS = {
|
|
|
254
254
|
},
|
|
255
255
|
'tab.open': (host, p) => {
|
|
256
256
|
const url = requireString(p, 'url');
|
|
257
|
-
|
|
257
|
+
const label = optionalString(p, 'agentLabel');
|
|
258
|
+
return host.openTab(url, { kind: 'agent', ...(label !== undefined ? { label } : {}) });
|
|
258
259
|
},
|
|
259
260
|
'tab.close': async (host, p) => {
|
|
260
261
|
const id = requireString(p, 'id');
|
|
@@ -547,6 +548,10 @@ function bindBrowserOp(body, identity) {
|
|
|
547
548
|
params.agentId = identity.agentId;
|
|
548
549
|
if (identity.planId !== undefined)
|
|
549
550
|
params.planId = identity.planId;
|
|
551
|
+
if (identity.agentLabel !== undefined)
|
|
552
|
+
params.agentLabel = identity.agentLabel;
|
|
553
|
+
else
|
|
554
|
+
delete params.agentLabel;
|
|
550
555
|
if (typeof params.tabId !== 'string' || !params.tabId) {
|
|
551
556
|
if (identity.defaultTabId !== undefined)
|
|
552
557
|
params.tabId = identity.defaultTabId;
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
// Copyright (c) 2025-present Minionry, Inc.
|
|
2
2
|
// SPDX-License-Identifier: LicenseRef-Minionry-Software
|
|
3
3
|
import { spawn } from 'node:child_process';
|
|
4
|
-
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
4
|
import { Hono } from 'hono';
|
|
7
|
-
import { resolveClaudeCommand } from '../engines/claude/claude-command.js';
|
|
8
|
-
import { spacePath } from '../services/space-layout.js';
|
|
5
|
+
import { resolveClaudeCommand, TOOL_LESS_CLAUDE_ARGS } from '../engines/claude/claude-command.js';
|
|
9
6
|
export function createNotificationRoutes(workingDir) {
|
|
10
7
|
const routes = new Hono();
|
|
11
8
|
routes.post('/summarize', async (c) => {
|
|
@@ -27,10 +24,6 @@ export function createNotificationRoutes(workingDir) {
|
|
|
27
24
|
}
|
|
28
25
|
async function generateNotificationSummary(userPrompt, output, workingDir) {
|
|
29
26
|
return new Promise((resolve) => {
|
|
30
|
-
const tempDir = spacePath(workingDir, 'tmp');
|
|
31
|
-
if (!existsSync(tempDir)) {
|
|
32
|
-
mkdirSync(tempDir, { recursive: true });
|
|
33
|
-
}
|
|
34
27
|
let truncatedOutput = output;
|
|
35
28
|
if (output.length > 4000) {
|
|
36
29
|
const firstPart = output.slice(0, 2000);
|
|
@@ -55,19 +48,19 @@ Generate a notification summary following these rules:
|
|
|
55
48
|
6. No emojis, no markdown, just plain text
|
|
56
49
|
|
|
57
50
|
Respond with ONLY the summary text, nothing else.`;
|
|
58
|
-
const promptFile = join(tempDir, `notif-summary-${Date.now()}.txt`);
|
|
59
|
-
writeFileSync(promptFile, summaryPrompt);
|
|
60
51
|
const systemPrompt = 'You are a notification summary assistant. Respond with only the summary text, no preamble or explanation.';
|
|
61
52
|
const args = [
|
|
62
53
|
'--print',
|
|
63
54
|
'--model', 'haiku',
|
|
64
|
-
|
|
65
|
-
|
|
55
|
+
...TOOL_LESS_CLAUDE_ARGS,
|
|
56
|
+
'--system-prompt', systemPrompt
|
|
66
57
|
];
|
|
67
58
|
const claude = spawn(resolveClaudeCommand(), args, {
|
|
68
59
|
cwd: workingDir,
|
|
69
|
-
stdio: ['
|
|
60
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
70
61
|
});
|
|
62
|
+
claude.stdin?.on('error', () => { });
|
|
63
|
+
claude.stdin?.end(summaryPrompt);
|
|
71
64
|
let stdout = '';
|
|
72
65
|
let stderr = '';
|
|
73
66
|
claude.stdout?.on('data', (data) => {
|
|
@@ -77,11 +70,6 @@ Respond with ONLY the summary text, nothing else.`;
|
|
|
77
70
|
stderr += data.toString();
|
|
78
71
|
});
|
|
79
72
|
claude.on('close', (code) => {
|
|
80
|
-
try {
|
|
81
|
-
unlinkSync(promptFile);
|
|
82
|
-
}
|
|
83
|
-
catch {
|
|
84
|
-
}
|
|
85
73
|
if (code === 0 && stdout.trim()) {
|
|
86
74
|
const summary = stdout.trim().slice(0, 150);
|
|
87
75
|
resolve(summary);
|