@dalmasonto/taskflow-mcp 1.0.18 → 1.0.19
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/dist/agent-registry.js +6 -2
- package/dist/config.js +8 -1
- package/dist/index.js +3 -1
- package/dist/sse.js +39 -14
- package/dist/tools/agent-inbox.d.ts +5 -0
- package/dist/tools/agent-inbox.js +20 -3
- package/dist/tools/terminal.d.ts +2 -0
- package/dist/tools/terminal.js +27 -2
- package/package.json +1 -1
package/dist/agent-registry.js
CHANGED
|
@@ -57,8 +57,12 @@ export function registerAgent(options) {
|
|
|
57
57
|
checkAgentLiveness();
|
|
58
58
|
// Find all entries for this project path
|
|
59
59
|
const entries = db.prepare('SELECT * FROM agent_registry WHERE project_path = ? ORDER BY connected_at DESC').all(projectPath);
|
|
60
|
-
// Priority 1: Reuse a disconnected entry (
|
|
61
|
-
|
|
60
|
+
// Priority 1: Reuse a disconnected entry that had the SAME PID (exact session resume)
|
|
61
|
+
// Priority 2: Reuse any disconnected entry for this path (new session, same project)
|
|
62
|
+
// This prevents a reconnecting agent from stealing another agent's identity
|
|
63
|
+
// when two sessions share the same project directory.
|
|
64
|
+
const disconnected = entries.find(e => e.status === 'disconnected' && e.pid === agentPid)
|
|
65
|
+
|| entries.find(e => e.status === 'disconnected');
|
|
62
66
|
if (disconnected) {
|
|
63
67
|
const newName = options?.customName || disconnected.name;
|
|
64
68
|
const renamed = newName !== disconnected.name;
|
package/dist/config.js
CHANGED
|
@@ -24,7 +24,14 @@ const DEFAULTS = {
|
|
|
24
24
|
relayPushToken: '',
|
|
25
25
|
};
|
|
26
26
|
// ─── config file path ────────────────────────────────────────────────
|
|
27
|
-
|
|
27
|
+
// Config lives alongside the database in ~/.taskflow/
|
|
28
|
+
// Falls back to legacy ~/.taskflow_config.json for backward compat
|
|
29
|
+
import { existsSync } from 'fs';
|
|
30
|
+
const NEW_CONFIG_PATH = resolve(homedir(), '.taskflow', 'config.json');
|
|
31
|
+
const LEGACY_CONFIG_PATH = resolve(homedir(), '.taskflow_config.json');
|
|
32
|
+
const CONFIG_PATH = existsSync(NEW_CONFIG_PATH) ? NEW_CONFIG_PATH
|
|
33
|
+
: existsSync(LEGACY_CONFIG_PATH) ? LEGACY_CONFIG_PATH
|
|
34
|
+
: NEW_CONFIG_PATH; // default to new path for fresh installs
|
|
28
35
|
// ─── loader ──────────────────────────────────────────────────────────
|
|
29
36
|
function loadFromFile() {
|
|
30
37
|
try {
|
package/dist/index.js
CHANGED
|
@@ -140,8 +140,10 @@ if (!httpOnly) {
|
|
|
140
140
|
const transport = new StdioServerTransport();
|
|
141
141
|
await server.connect(transport);
|
|
142
142
|
const { registerAgent, unregisterAgent } = await import('./agent-registry.js');
|
|
143
|
-
|
|
143
|
+
const { setAgentName } = await import('./tools/agent-inbox.js');
|
|
144
|
+
// Auto-register this agent and sync name to agent-inbox tools
|
|
144
145
|
const agentName = registerAgent();
|
|
146
|
+
setAgentName(agentName);
|
|
145
147
|
const agentPid = process.ppid;
|
|
146
148
|
console.error(`[agent] registered as "${agentName}"`);
|
|
147
149
|
let cleanup = () => { try {
|
package/dist/sse.js
CHANGED
|
@@ -3,6 +3,7 @@ import { execFileSync } from 'child_process';
|
|
|
3
3
|
import { getDb } from './db.js';
|
|
4
4
|
import { logActivity } from './helpers.js';
|
|
5
5
|
import { getConfig } from './config.js';
|
|
6
|
+
import { validateKeys } from './tools/terminal.js';
|
|
6
7
|
const SERVICE_ID = 'taskflow-mcp';
|
|
7
8
|
const PROBE_TIMEOUT_MS = 2000;
|
|
8
9
|
// ─── Relay upstream config (from config file, env vars, or .env) ────
|
|
@@ -53,24 +54,32 @@ function readBody(req) {
|
|
|
53
54
|
req.on('end', () => resolve(body));
|
|
54
55
|
});
|
|
55
56
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Detect an active Claude Code permission prompt.
|
|
59
|
+
*
|
|
60
|
+
* The definitive marker is "Esc to cancel" which only appears in the
|
|
61
|
+
* footer of active prompts and disappears once the user responds.
|
|
62
|
+
* We check only the last 10 lines of the VISIBLE screen (not full
|
|
63
|
+
* scrollback) to avoid matching old, already-answered prompts.
|
|
64
|
+
*
|
|
65
|
+
* Generic patterns like "Do you want to" and "(y/n)" are intentionally
|
|
66
|
+
* excluded — Claude writes these in prose responses too, causing false flags.
|
|
67
|
+
*/
|
|
64
68
|
function detectPrompt(content) {
|
|
65
|
-
|
|
69
|
+
// Only check last 10 lines — the prompt footer is always at the bottom
|
|
70
|
+
const lines = content.split('\n');
|
|
71
|
+
const tail = lines.slice(-10).join('\n');
|
|
66
72
|
const hints = [];
|
|
67
|
-
|
|
73
|
+
const hasEsc = /Esc to cancel/i.test(tail);
|
|
74
|
+
const hasTab = /Tab to amend/i.test(tail);
|
|
75
|
+
if (hasEsc)
|
|
68
76
|
hints.push('Esc to cancel');
|
|
69
|
-
if (
|
|
77
|
+
if (hasTab)
|
|
70
78
|
hints.push('Tab to amend');
|
|
71
79
|
if (/shift\+tab/i.test(tail))
|
|
72
80
|
hints.push('Shift+Tab');
|
|
73
|
-
|
|
81
|
+
// Only flag as detected if we see the actual prompt footer
|
|
82
|
+
const detected = hasEsc || hasTab;
|
|
74
83
|
return { detected, hints };
|
|
75
84
|
}
|
|
76
85
|
/** Fast string hash (djb2) for content change detection */
|
|
@@ -466,6 +475,10 @@ export async function startSSEServer() {
|
|
|
466
475
|
jsonResponse(res, 400, { error: `Agent "${agentName}" has no tmux pane` });
|
|
467
476
|
return;
|
|
468
477
|
}
|
|
478
|
+
if (agent.status !== 'connected') {
|
|
479
|
+
jsonResponse(res, 403, { error: `Agent "${agentName}" is disconnected — sending keys to a bare shell is blocked for security` });
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
469
482
|
let body;
|
|
470
483
|
try {
|
|
471
484
|
body = JSON.parse(await readBody(req));
|
|
@@ -481,6 +494,12 @@ export async function startSSEServer() {
|
|
|
481
494
|
jsonResponse(res, 400, { error: 'keys must be a string' });
|
|
482
495
|
return;
|
|
483
496
|
}
|
|
497
|
+
// Block shell escape patterns and oversized payloads
|
|
498
|
+
const violation = validateKeys(keys);
|
|
499
|
+
if (violation) {
|
|
500
|
+
jsonResponse(res, 403, { error: violation });
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
484
503
|
try {
|
|
485
504
|
const pane = agent.tmux_pane;
|
|
486
505
|
if (literal) {
|
|
@@ -582,8 +601,14 @@ export async function startSSEServer() {
|
|
|
582
601
|
'Authorization': `Bearer ${RELAY_PUSH_TOKEN}`,
|
|
583
602
|
},
|
|
584
603
|
body: JSON.stringify({ url: localUrl }),
|
|
585
|
-
}).then(() => {
|
|
586
|
-
|
|
604
|
+
}).then(async (res) => {
|
|
605
|
+
if (res.ok) {
|
|
606
|
+
console.log(`[SSE] registered with relay at ${RELAY_URL}`);
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
const body = await res.text().catch(() => '');
|
|
610
|
+
console.error(`[SSE] relay registration rejected: ${res.status} ${body}`);
|
|
611
|
+
}
|
|
587
612
|
}).catch((err) => {
|
|
588
613
|
console.error(`[SSE] relay registration failed: ${err.message}`);
|
|
589
614
|
});
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
/** The name assigned to this agent after registration */
|
|
3
3
|
declare let myAgentName: string | null;
|
|
4
|
+
/** Set the agent name from external registration (index.ts).
|
|
5
|
+
* This prevents double-registration when index.ts registers at startup
|
|
6
|
+
* and ensureRegistered() would register again on first tool call.
|
|
7
|
+
*/
|
|
8
|
+
export declare function setAgentName(name: string): void;
|
|
4
9
|
export { myAgentName };
|
|
5
10
|
export declare function registerAgentInboxTools(server: McpServer): void;
|
|
@@ -4,11 +4,28 @@ import { logActivity, errorResponse, successResponse, now, broadcastChange } fro
|
|
|
4
4
|
import { registerAgent as doRegister, getAgent, listAgents } from '../agent-registry.js';
|
|
5
5
|
/** The name assigned to this agent after registration */
|
|
6
6
|
let myAgentName = null;
|
|
7
|
-
/**
|
|
7
|
+
/** Set the agent name from external registration (index.ts).
|
|
8
|
+
* This prevents double-registration when index.ts registers at startup
|
|
9
|
+
* and ensureRegistered() would register again on first tool call.
|
|
10
|
+
*/
|
|
11
|
+
export function setAgentName(name) {
|
|
12
|
+
myAgentName = name;
|
|
13
|
+
}
|
|
14
|
+
/** Get or auto-register the agent name.
|
|
15
|
+
* Verifies the cached registration is still valid — if the liveness
|
|
16
|
+
* checker disconnected us while sleeping, re-register to reclaim identity.
|
|
17
|
+
*/
|
|
8
18
|
function ensureRegistered() {
|
|
9
|
-
if (
|
|
10
|
-
|
|
19
|
+
if (myAgentName) {
|
|
20
|
+
// Verify our registration is still active
|
|
21
|
+
const agent = getAgent(myAgentName);
|
|
22
|
+
if (agent && agent.status === 'connected' && agent.pid === process.ppid) {
|
|
23
|
+
return myAgentName;
|
|
24
|
+
}
|
|
25
|
+
// Stale — re-register
|
|
26
|
+
myAgentName = null;
|
|
11
27
|
}
|
|
28
|
+
myAgentName = doRegister();
|
|
12
29
|
return myAgentName;
|
|
13
30
|
}
|
|
14
31
|
export { myAgentName };
|
package/dist/tools/terminal.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
/** Validate keys before sending — blocks shell escape patterns */
|
|
3
|
+
export declare function validateKeys(keys: string): string | null;
|
|
2
4
|
export declare function registerTerminalTools(server: McpServer): void;
|
package/dist/tools/terminal.js
CHANGED
|
@@ -2,13 +2,34 @@ import { z } from 'zod';
|
|
|
2
2
|
import { execFileSync } from 'child_process';
|
|
3
3
|
import { getDb } from '../db.js';
|
|
4
4
|
import { logActivity, errorResponse, successResponse } from '../helpers.js';
|
|
5
|
-
|
|
5
|
+
const MAX_KEYS_LENGTH = 200;
|
|
6
|
+
// Patterns that should never be sent via remote key injection
|
|
7
|
+
const BLOCKED_PATTERNS = [
|
|
8
|
+
/^\s*!/, // Claude Code shell escape (! command)
|
|
9
|
+
/;\s*!/, // shell escape after semicolon
|
|
10
|
+
];
|
|
11
|
+
/** Validate keys before sending — blocks shell escape patterns */
|
|
12
|
+
export function validateKeys(keys) {
|
|
13
|
+
if (keys.length > MAX_KEYS_LENGTH) {
|
|
14
|
+
return `Keys too long (${keys.length} chars, max ${MAX_KEYS_LENGTH}) — potential injection`;
|
|
15
|
+
}
|
|
16
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
17
|
+
if (pattern.test(keys)) {
|
|
18
|
+
return 'Blocked: shell escape pattern detected (! prefix). Use the terminal directly for shell commands.';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null; // valid
|
|
22
|
+
}
|
|
23
|
+
function getAgentPane(agentName, requireConnected = false) {
|
|
6
24
|
const db = getDb();
|
|
7
25
|
const agent = db.prepare('SELECT * FROM agent_registry WHERE name = ?').get(agentName);
|
|
8
26
|
if (!agent)
|
|
9
27
|
return { error: errorResponse(`Agent "${agentName}" not found`, 'NOT_FOUND') };
|
|
10
28
|
if (!agent.tmux_pane)
|
|
11
29
|
return { error: errorResponse(`Agent "${agentName}" has no tmux pane`, 'VALIDATION_ERROR') };
|
|
30
|
+
if (requireConnected && agent.status !== 'connected') {
|
|
31
|
+
return { error: errorResponse(`Agent "${agentName}" is disconnected — sending keys to a bare shell is blocked for security`, 'VALIDATION_ERROR') };
|
|
32
|
+
}
|
|
12
33
|
return { agent };
|
|
13
34
|
}
|
|
14
35
|
export function registerTerminalTools(server) {
|
|
@@ -36,9 +57,13 @@ export function registerTerminalTools(server) {
|
|
|
36
57
|
enter: z.boolean().optional().describe('Whether to press Enter after the keys (default: true)'),
|
|
37
58
|
literal: z.boolean().optional().describe('Send as literal text with -l flag (default: true). Set false for tmux key names like Escape, Up, Down, Left, Right, BTab'),
|
|
38
59
|
}, { destructiveHint: true }, async (params) => {
|
|
39
|
-
const result = getAgentPane(params.agent_name);
|
|
60
|
+
const result = getAgentPane(params.agent_name, true);
|
|
40
61
|
if (result.error)
|
|
41
62
|
return result.error;
|
|
63
|
+
// Validate keys for injection attacks
|
|
64
|
+
const violation = validateKeys(params.keys);
|
|
65
|
+
if (violation)
|
|
66
|
+
return errorResponse(violation, 'VALIDATION_ERROR');
|
|
42
67
|
const sendEnter = params.enter !== false;
|
|
43
68
|
const isLiteral = params.literal !== false;
|
|
44
69
|
try {
|