@herdctl/core 3.0.2 → 4.1.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/dist/config/__tests__/schema.test.js +45 -0
- package/dist/config/__tests__/schema.test.js.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +4 -2
- package/dist/config/index.js.map +1 -1
- package/dist/config/schema.d.ts +774 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +100 -1
- package/dist/config/schema.js.map +1 -1
- package/dist/fleet-manager/__tests__/discord-manager.test.js +1415 -84
- package/dist/fleet-manager/__tests__/discord-manager.test.js.map +1 -1
- package/dist/fleet-manager/__tests__/slack-manager.test.d.ts +11 -0
- package/dist/fleet-manager/__tests__/slack-manager.test.d.ts.map +1 -0
- package/dist/fleet-manager/__tests__/slack-manager.test.js +1022 -0
- package/dist/fleet-manager/__tests__/slack-manager.test.js.map +1 -0
- package/dist/fleet-manager/context.d.ts +4 -0
- package/dist/fleet-manager/context.d.ts.map +1 -1
- package/dist/fleet-manager/discord-manager.d.ts +75 -2
- package/dist/fleet-manager/discord-manager.d.ts.map +1 -1
- package/dist/fleet-manager/discord-manager.js +374 -3
- package/dist/fleet-manager/discord-manager.js.map +1 -1
- package/dist/fleet-manager/event-types.d.ts +113 -0
- package/dist/fleet-manager/event-types.d.ts.map +1 -1
- package/dist/fleet-manager/fleet-manager.d.ts +3 -0
- package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
- package/dist/fleet-manager/fleet-manager.js +10 -0
- package/dist/fleet-manager/fleet-manager.js.map +1 -1
- package/dist/fleet-manager/job-control.d.ts.map +1 -1
- package/dist/fleet-manager/job-control.js +5 -2
- package/dist/fleet-manager/job-control.js.map +1 -1
- package/dist/fleet-manager/slack-manager.d.ts +158 -0
- package/dist/fleet-manager/slack-manager.d.ts.map +1 -0
- package/dist/fleet-manager/slack-manager.js +570 -0
- package/dist/fleet-manager/slack-manager.js.map +1 -0
- package/dist/fleet-manager/status-queries.d.ts +2 -1
- package/dist/fleet-manager/status-queries.d.ts.map +1 -1
- package/dist/fleet-manager/status-queries.js +42 -3
- package/dist/fleet-manager/status-queries.js.map +1 -1
- package/dist/fleet-manager/types.d.ts +43 -3
- package/dist/fleet-manager/types.d.ts.map +1 -1
- package/dist/hooks/__tests__/slack-runner.test.d.ts +5 -0
- package/dist/hooks/__tests__/slack-runner.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/slack-runner.test.js +307 -0
- package/dist/hooks/__tests__/slack-runner.test.js.map +1 -0
- package/dist/hooks/hook-executor.d.ts +1 -0
- package/dist/hooks/hook-executor.d.ts.map +1 -1
- package/dist/hooks/hook-executor.js +8 -0
- package/dist/hooks/hook-executor.js.map +1 -1
- package/dist/hooks/index.d.ts +2 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +2 -0
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/runners/slack.d.ts +62 -0
- package/dist/hooks/runners/slack.d.ts.map +1 -0
- package/dist/hooks/runners/slack.js +329 -0
- package/dist/hooks/runners/slack.js.map +1 -0
- package/dist/hooks/types.d.ts +4 -4
- package/dist/hooks/types.d.ts.map +1 -1
- package/dist/runner/__tests__/file-sender-mcp.test.d.ts +2 -0
- package/dist/runner/__tests__/file-sender-mcp.test.d.ts.map +1 -0
- package/dist/runner/__tests__/file-sender-mcp.test.js +177 -0
- package/dist/runner/__tests__/file-sender-mcp.test.js.map +1 -0
- package/dist/runner/__tests__/job-executor.test.js +12 -12
- package/dist/runner/__tests__/job-executor.test.js.map +1 -1
- package/dist/runner/file-sender-mcp.d.ts +69 -0
- package/dist/runner/file-sender-mcp.d.ts.map +1 -0
- package/dist/runner/file-sender-mcp.js +145 -0
- package/dist/runner/file-sender-mcp.js.map +1 -0
- package/dist/runner/index.d.ts +1 -0
- package/dist/runner/index.d.ts.map +1 -1
- package/dist/runner/index.js +2 -0
- package/dist/runner/index.js.map +1 -1
- package/dist/runner/job-executor.d.ts.map +1 -1
- package/dist/runner/job-executor.js +35 -5
- package/dist/runner/job-executor.js.map +1 -1
- package/dist/runner/runtime/__tests__/docker-security.test.js +12 -12
- package/dist/runner/runtime/__tests__/docker-security.test.js.map +1 -1
- package/dist/runner/runtime/__tests__/mcp-http-bridge.test.d.ts +2 -0
- package/dist/runner/runtime/__tests__/mcp-http-bridge.test.d.ts.map +1 -0
- package/dist/runner/runtime/__tests__/mcp-http-bridge.test.js +191 -0
- package/dist/runner/runtime/__tests__/mcp-http-bridge.test.js.map +1 -0
- package/dist/runner/runtime/container-manager.d.ts +5 -1
- package/dist/runner/runtime/container-manager.d.ts.map +1 -1
- package/dist/runner/runtime/container-manager.js +115 -5
- package/dist/runner/runtime/container-manager.js.map +1 -1
- package/dist/runner/runtime/container-runner.d.ts +2 -0
- package/dist/runner/runtime/container-runner.d.ts.map +1 -1
- package/dist/runner/runtime/container-runner.js +121 -74
- package/dist/runner/runtime/container-runner.js.map +1 -1
- package/dist/runner/runtime/index.d.ts +1 -0
- package/dist/runner/runtime/index.d.ts.map +1 -1
- package/dist/runner/runtime/index.js +2 -0
- package/dist/runner/runtime/index.js.map +1 -1
- package/dist/runner/runtime/interface.d.ts +2 -0
- package/dist/runner/runtime/interface.d.ts.map +1 -1
- package/dist/runner/runtime/mcp-http-bridge.d.ts +39 -0
- package/dist/runner/runtime/mcp-http-bridge.d.ts.map +1 -0
- package/dist/runner/runtime/mcp-http-bridge.js +205 -0
- package/dist/runner/runtime/mcp-http-bridge.js.map +1 -0
- package/dist/runner/runtime/sdk-runtime.d.ts.map +1 -1
- package/dist/runner/runtime/sdk-runtime.js +74 -1
- package/dist/runner/runtime/sdk-runtime.js.map +1 -1
- package/dist/runner/types.d.ts +44 -0
- package/dist/runner/types.d.ts.map +1 -1
- package/dist/state/index.d.ts +1 -1
- package/dist/state/index.d.ts.map +1 -1
- package/dist/state/index.js +1 -1
- package/dist/state/index.js.map +1 -1
- package/dist/state/session-validation.d.ts +8 -0
- package/dist/state/session-validation.d.ts.map +1 -1
- package/dist/state/session-validation.js +36 -0
- package/dist/state/session-validation.js.map +1 -1
- package/package.json +1 -9
package/dist/hooks/index.d.ts
CHANGED
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
*
|
|
8
8
|
* @module hooks
|
|
9
9
|
*/
|
|
10
|
-
export type { HookContext, HookResult, BaseHookConfig, AgentHooksConfig, HookRunner, ShellHookConfigInput, WebhookHookConfigInput, DiscordHookConfigInput, HookConfigInput, } from "./types.js";
|
|
10
|
+
export type { HookContext, HookResult, BaseHookConfig, AgentHooksConfig, HookRunner, ShellHookConfigInput, WebhookHookConfigInput, DiscordHookConfigInput, SlackHookConfigInput, HookConfigInput, } from "./types.js";
|
|
11
11
|
export { HookExecutor, type HookExecutorOptions, type HookExecutorLogger, type HookExecutionResult, } from "./hook-executor.js";
|
|
12
12
|
export { ShellHookRunner, type ShellHookRunnerOptions, type ShellHookRunnerLogger, } from "./runners/shell.js";
|
|
13
13
|
export { WebhookHookRunner, type WebhookHookRunnerOptions, type WebhookHookRunnerLogger, } from "./runners/webhook.js";
|
|
14
14
|
export { DiscordHookRunner, type DiscordHookRunnerOptions, type DiscordHookRunnerLogger, } from "./runners/discord.js";
|
|
15
|
+
export { SlackHookRunner, type SlackHookRunnerOptions, type SlackHookRunnerLogger, } from "./runners/slack.js";
|
|
15
16
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,YAAY,EACV,WAAW,EACX,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,UAAU,EAEV,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,EACtB,eAAe,GAChB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,eAAe,EACf,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,iBAAiB,EACjB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,iBAAiB,EACjB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,sBAAsB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,YAAY,EACV,WAAW,EACX,UAAU,EACV,cAAc,EACd,gBAAgB,EAChB,UAAU,EAEV,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,EACpB,eAAe,GAChB,MAAM,YAAY,CAAC;AAGpB,OAAO,EACL,YAAY,EACZ,KAAK,mBAAmB,EACxB,KAAK,kBAAkB,EACvB,KAAK,mBAAmB,GACzB,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,eAAe,EACf,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EACL,iBAAiB,EACjB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,iBAAiB,EACjB,KAAK,wBAAwB,EAC7B,KAAK,uBAAuB,GAC7B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,eAAe,EACf,KAAK,sBAAsB,EAC3B,KAAK,qBAAqB,GAC3B,MAAM,oBAAoB,CAAC"}
|
package/dist/hooks/index.js
CHANGED
|
@@ -15,4 +15,6 @@ export { ShellHookRunner, } from "./runners/shell.js";
|
|
|
15
15
|
export { WebhookHookRunner, } from "./runners/webhook.js";
|
|
16
16
|
// Discord Hook Runner
|
|
17
17
|
export { DiscordHookRunner, } from "./runners/discord.js";
|
|
18
|
+
// Slack Hook Runner
|
|
19
|
+
export { SlackHookRunner, } from "./runners/slack.js";
|
|
18
20
|
//# sourceMappingURL=index.js.map
|
package/dist/hooks/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAmBH,gBAAgB;AAChB,OAAO,EACL,YAAY,GAIb,MAAM,oBAAoB,CAAC;AAE5B,oBAAoB;AACpB,OAAO,EACL,eAAe,GAGhB,MAAM,oBAAoB,CAAC;AAE5B,sBAAsB;AACtB,OAAO,EACL,iBAAiB,GAGlB,MAAM,sBAAsB,CAAC;AAE9B,sBAAsB;AACtB,OAAO,EACL,iBAAiB,GAGlB,MAAM,sBAAsB,CAAC;AAE9B,oBAAoB;AACpB,OAAO,EACL,eAAe,GAGhB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack Hook Runner
|
|
3
|
+
*
|
|
4
|
+
* Posts job notifications to a Slack channel using rich message formatting.
|
|
5
|
+
* Used for team visibility into fleet activity.
|
|
6
|
+
*/
|
|
7
|
+
import type { HookContext, HookResult, SlackHookConfigInput } from "../types.js";
|
|
8
|
+
/**
|
|
9
|
+
* Logger interface for SlackHookRunner
|
|
10
|
+
*/
|
|
11
|
+
export interface SlackHookRunnerLogger {
|
|
12
|
+
debug: (message: string) => void;
|
|
13
|
+
info: (message: string) => void;
|
|
14
|
+
warn: (message: string) => void;
|
|
15
|
+
error: (message: string) => void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Options for SlackHookRunner
|
|
19
|
+
*/
|
|
20
|
+
export interface SlackHookRunnerOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Logger for hook execution output
|
|
23
|
+
*/
|
|
24
|
+
logger?: SlackHookRunnerLogger;
|
|
25
|
+
/**
|
|
26
|
+
* Custom fetch implementation (for testing)
|
|
27
|
+
*/
|
|
28
|
+
fetch?: typeof globalThis.fetch;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* SlackHookRunner posts job notifications to a Slack channel
|
|
32
|
+
*
|
|
33
|
+
* Uses the Slack Web API (chat.postMessage) with attachments for rich formatting.
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```typescript
|
|
37
|
+
* const runner = new SlackHookRunner({ logger: console });
|
|
38
|
+
*
|
|
39
|
+
* const result = await runner.execute(
|
|
40
|
+
* {
|
|
41
|
+
* type: 'slack',
|
|
42
|
+
* channel_id: 'C1234567890',
|
|
43
|
+
* bot_token_env: 'SLACK_BOT_TOKEN'
|
|
44
|
+
* },
|
|
45
|
+
* hookContext
|
|
46
|
+
* );
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare class SlackHookRunner {
|
|
50
|
+
private logger;
|
|
51
|
+
private fetchFn;
|
|
52
|
+
constructor(options?: SlackHookRunnerOptions);
|
|
53
|
+
/**
|
|
54
|
+
* Execute a Slack hook with the given context
|
|
55
|
+
*
|
|
56
|
+
* @param config - Slack hook configuration
|
|
57
|
+
* @param context - Hook context to send in the notification
|
|
58
|
+
* @returns Promise resolving to the hook result
|
|
59
|
+
*/
|
|
60
|
+
execute(config: SlackHookConfigInput, context: HookContext): Promise<HookResult>;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=slack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../../src/hooks/runners/slack.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AA2BjF;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC;AAED;;GAEG;AACH,MAAM,WAAW,sBAAsB;IACrC;;OAEG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;IAE/B;;OAEG;IACH,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC;AAwKD;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,eAAe;IAC1B,OAAO,CAAC,MAAM,CAAwB;IACtC,OAAO,CAAC,OAAO,CAA0B;gBAE7B,OAAO,GAAE,sBAA2B;IAUhD;;;;;;OAMG;IACG,OAAO,CAAC,MAAM,EAAE,oBAAoB,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC;CAoJvF"}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slack Hook Runner
|
|
3
|
+
*
|
|
4
|
+
* Posts job notifications to a Slack channel using rich message formatting.
|
|
5
|
+
* Used for team visibility into fleet activity.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Default timeout for Slack API requests in milliseconds
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_TIMEOUT = 10000;
|
|
11
|
+
/**
|
|
12
|
+
* Maximum output length for message text (Slack limit: ~40K, practical: 4000)
|
|
13
|
+
*/
|
|
14
|
+
const MAX_TEXT_LENGTH = 3500;
|
|
15
|
+
/**
|
|
16
|
+
* Maximum length for attachment fields
|
|
17
|
+
*/
|
|
18
|
+
const MAX_FIELD_LENGTH = 900;
|
|
19
|
+
/**
|
|
20
|
+
* Colors for different event types
|
|
21
|
+
*/
|
|
22
|
+
const EVENT_COLORS = {
|
|
23
|
+
completed: "#22c55e", // green
|
|
24
|
+
failed: "#ef4444", // red
|
|
25
|
+
timeout: "#f59e0b", // amber
|
|
26
|
+
cancelled: "#6b7280", // gray
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Truncates a string to a maximum length, adding ellipsis if truncated
|
|
30
|
+
*/
|
|
31
|
+
function truncateOutput(output, maxLength) {
|
|
32
|
+
if (output.length <= maxLength) {
|
|
33
|
+
return output;
|
|
34
|
+
}
|
|
35
|
+
return output.slice(0, maxLength - 3) + "...";
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Formats duration in milliseconds to a human-readable string
|
|
39
|
+
*/
|
|
40
|
+
function formatDuration(ms) {
|
|
41
|
+
if (ms < 1000) {
|
|
42
|
+
return `${ms}ms`;
|
|
43
|
+
}
|
|
44
|
+
const seconds = Math.floor(ms / 1000);
|
|
45
|
+
if (seconds < 60) {
|
|
46
|
+
return `${seconds}s`;
|
|
47
|
+
}
|
|
48
|
+
const minutes = Math.floor(seconds / 60);
|
|
49
|
+
const remainingSeconds = seconds % 60;
|
|
50
|
+
if (minutes < 60) {
|
|
51
|
+
return remainingSeconds > 0 ? `${minutes}m ${remainingSeconds}s` : `${minutes}m`;
|
|
52
|
+
}
|
|
53
|
+
const hours = Math.floor(minutes / 60);
|
|
54
|
+
const remainingMinutes = minutes % 60;
|
|
55
|
+
return remainingMinutes > 0 ? `${hours}h ${remainingMinutes}m` : `${hours}h`;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Gets the title for the event
|
|
59
|
+
*/
|
|
60
|
+
function getEventTitle(event) {
|
|
61
|
+
switch (event) {
|
|
62
|
+
case "completed":
|
|
63
|
+
return "Job Completed";
|
|
64
|
+
case "failed":
|
|
65
|
+
return "Job Failed";
|
|
66
|
+
case "timeout":
|
|
67
|
+
return "Job Timed Out";
|
|
68
|
+
case "cancelled":
|
|
69
|
+
return "Job Cancelled";
|
|
70
|
+
default:
|
|
71
|
+
return "Job Event";
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Gets the fallback text for the event (plain text for notifications)
|
|
76
|
+
*/
|
|
77
|
+
function getEventFallback(event, agentName) {
|
|
78
|
+
switch (event) {
|
|
79
|
+
case "completed":
|
|
80
|
+
return `Job completed for ${agentName}`;
|
|
81
|
+
case "failed":
|
|
82
|
+
return `Job failed for ${agentName}`;
|
|
83
|
+
case "timeout":
|
|
84
|
+
return `Job timed out for ${agentName}`;
|
|
85
|
+
case "cancelled":
|
|
86
|
+
return `Job cancelled for ${agentName}`;
|
|
87
|
+
default:
|
|
88
|
+
return `Job event for ${agentName}`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Builds a Slack attachment from the hook context
|
|
93
|
+
*/
|
|
94
|
+
function buildAttachment(context) {
|
|
95
|
+
const agentName = context.agent.name || context.agent.id;
|
|
96
|
+
const fields = [
|
|
97
|
+
{
|
|
98
|
+
title: "Agent",
|
|
99
|
+
value: agentName,
|
|
100
|
+
short: true,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
title: "Job ID",
|
|
104
|
+
value: `\`${context.job.id}\``,
|
|
105
|
+
short: true,
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
title: "Duration",
|
|
109
|
+
value: formatDuration(context.job.durationMs),
|
|
110
|
+
short: true,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
// Add schedule name if present
|
|
114
|
+
if (context.job.scheduleName) {
|
|
115
|
+
fields.push({
|
|
116
|
+
title: "Schedule",
|
|
117
|
+
value: context.job.scheduleName,
|
|
118
|
+
short: true,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
// Add error message if present
|
|
122
|
+
if (context.result.error) {
|
|
123
|
+
fields.push({
|
|
124
|
+
title: "Error",
|
|
125
|
+
value: `\`\`\`${truncateOutput(context.result.error, MAX_FIELD_LENGTH)}\`\`\``,
|
|
126
|
+
short: false,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// Add metadata JSON if present
|
|
130
|
+
if (context.metadata && Object.keys(context.metadata).length > 0) {
|
|
131
|
+
fields.push({
|
|
132
|
+
title: "Metadata",
|
|
133
|
+
value: `\`\`\`${truncateOutput(JSON.stringify(context.metadata, null, 2), MAX_FIELD_LENGTH)}\`\`\``,
|
|
134
|
+
short: false,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// Build the attachment with output in text field
|
|
138
|
+
const output = context.result.output.trim();
|
|
139
|
+
let text;
|
|
140
|
+
if (output && output.length > 0) {
|
|
141
|
+
text = truncateOutput(output, MAX_TEXT_LENGTH);
|
|
142
|
+
}
|
|
143
|
+
return {
|
|
144
|
+
color: EVENT_COLORS[context.event] ?? EVENT_COLORS.completed,
|
|
145
|
+
fallback: getEventFallback(context.event, agentName),
|
|
146
|
+
title: getEventTitle(context.event),
|
|
147
|
+
text,
|
|
148
|
+
fields,
|
|
149
|
+
footer: "herdctl",
|
|
150
|
+
ts: Math.floor(new Date(context.job.completedAt).getTime() / 1000),
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* SlackHookRunner posts job notifications to a Slack channel
|
|
155
|
+
*
|
|
156
|
+
* Uses the Slack Web API (chat.postMessage) with attachments for rich formatting.
|
|
157
|
+
*
|
|
158
|
+
* @example
|
|
159
|
+
* ```typescript
|
|
160
|
+
* const runner = new SlackHookRunner({ logger: console });
|
|
161
|
+
*
|
|
162
|
+
* const result = await runner.execute(
|
|
163
|
+
* {
|
|
164
|
+
* type: 'slack',
|
|
165
|
+
* channel_id: 'C1234567890',
|
|
166
|
+
* bot_token_env: 'SLACK_BOT_TOKEN'
|
|
167
|
+
* },
|
|
168
|
+
* hookContext
|
|
169
|
+
* );
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
export class SlackHookRunner {
|
|
173
|
+
logger;
|
|
174
|
+
fetchFn;
|
|
175
|
+
constructor(options = {}) {
|
|
176
|
+
this.logger = options.logger ?? {
|
|
177
|
+
debug: () => { },
|
|
178
|
+
info: () => { },
|
|
179
|
+
warn: () => { },
|
|
180
|
+
error: () => { },
|
|
181
|
+
};
|
|
182
|
+
this.fetchFn = options.fetch ?? globalThis.fetch;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Execute a Slack hook with the given context
|
|
186
|
+
*
|
|
187
|
+
* @param config - Slack hook configuration
|
|
188
|
+
* @param context - Hook context to send in the notification
|
|
189
|
+
* @returns Promise resolving to the hook result
|
|
190
|
+
*/
|
|
191
|
+
async execute(config, context) {
|
|
192
|
+
const startTime = Date.now();
|
|
193
|
+
this.logger.debug(`Executing Slack hook for channel: ${config.channel_id}`);
|
|
194
|
+
// Read bot token from environment variable
|
|
195
|
+
const tokenEnv = config.bot_token_env ?? "SLACK_BOT_TOKEN";
|
|
196
|
+
const botToken = process.env[tokenEnv];
|
|
197
|
+
if (!botToken) {
|
|
198
|
+
const durationMs = Date.now() - startTime;
|
|
199
|
+
const errorMessage = `Slack bot token not found in environment variable: ${tokenEnv}`;
|
|
200
|
+
this.logger.error(errorMessage);
|
|
201
|
+
return {
|
|
202
|
+
success: false,
|
|
203
|
+
hookType: "slack",
|
|
204
|
+
durationMs,
|
|
205
|
+
error: errorMessage,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
// Build the Slack attachment
|
|
210
|
+
const attachment = buildAttachment(context);
|
|
211
|
+
const payload = {
|
|
212
|
+
channel: config.channel_id,
|
|
213
|
+
attachments: [attachment],
|
|
214
|
+
};
|
|
215
|
+
// Slack Web API endpoint for posting messages
|
|
216
|
+
const url = "https://slack.com/api/chat.postMessage";
|
|
217
|
+
// Create abort controller for timeout
|
|
218
|
+
const controller = new AbortController();
|
|
219
|
+
const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT);
|
|
220
|
+
try {
|
|
221
|
+
const response = await this.fetchFn(url, {
|
|
222
|
+
method: "POST",
|
|
223
|
+
headers: {
|
|
224
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
225
|
+
Authorization: `Bearer ${botToken}`,
|
|
226
|
+
},
|
|
227
|
+
body: JSON.stringify(payload),
|
|
228
|
+
signal: controller.signal,
|
|
229
|
+
});
|
|
230
|
+
clearTimeout(timeoutId);
|
|
231
|
+
const durationMs = Date.now() - startTime;
|
|
232
|
+
// Read response body
|
|
233
|
+
let responseBody;
|
|
234
|
+
try {
|
|
235
|
+
responseBody = await response.text();
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
// Ignore response body read errors
|
|
239
|
+
}
|
|
240
|
+
// Slack API always returns 200 with ok: true/false in body
|
|
241
|
+
if (response.ok && responseBody) {
|
|
242
|
+
try {
|
|
243
|
+
const json = JSON.parse(responseBody);
|
|
244
|
+
if (json.ok) {
|
|
245
|
+
this.logger.info(`Slack hook completed successfully in ${durationMs}ms: channel ${config.channel_id}`);
|
|
246
|
+
return {
|
|
247
|
+
success: true,
|
|
248
|
+
hookType: "slack",
|
|
249
|
+
durationMs,
|
|
250
|
+
output: responseBody,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
// Slack API error (ok: false)
|
|
255
|
+
const errorDetail = `Slack API error: ${json.error || "unknown"}`;
|
|
256
|
+
this.logger.warn(`Slack hook failed: ${errorDetail}`);
|
|
257
|
+
return {
|
|
258
|
+
success: false,
|
|
259
|
+
hookType: "slack",
|
|
260
|
+
durationMs,
|
|
261
|
+
error: errorDetail,
|
|
262
|
+
output: responseBody,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// JSON parse error
|
|
268
|
+
const errorDetail = `Failed to parse Slack API response`;
|
|
269
|
+
this.logger.warn(`Slack hook failed: ${errorDetail}`);
|
|
270
|
+
return {
|
|
271
|
+
success: false,
|
|
272
|
+
hookType: "slack",
|
|
273
|
+
durationMs,
|
|
274
|
+
error: errorDetail,
|
|
275
|
+
output: responseBody,
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
// HTTP error
|
|
281
|
+
const errorDetail = `HTTP ${response.status}: ${response.statusText}`;
|
|
282
|
+
this.logger.warn(`Slack hook failed with status ${response.status}: ${errorDetail}`);
|
|
283
|
+
return {
|
|
284
|
+
success: false,
|
|
285
|
+
hookType: "slack",
|
|
286
|
+
durationMs,
|
|
287
|
+
error: errorDetail,
|
|
288
|
+
output: responseBody,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch (fetchError) {
|
|
293
|
+
clearTimeout(timeoutId);
|
|
294
|
+
const durationMs = Date.now() - startTime;
|
|
295
|
+
// Handle abort (timeout)
|
|
296
|
+
if (fetchError instanceof Error && fetchError.name === "AbortError") {
|
|
297
|
+
this.logger.error(`Slack hook timed out after ${DEFAULT_TIMEOUT}ms`);
|
|
298
|
+
return {
|
|
299
|
+
success: false,
|
|
300
|
+
hookType: "slack",
|
|
301
|
+
durationMs,
|
|
302
|
+
error: `Slack hook timed out after ${DEFAULT_TIMEOUT}ms`,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// Handle other fetch errors (network errors, etc.)
|
|
306
|
+
const errorMessage = fetchError instanceof Error ? fetchError.message : String(fetchError);
|
|
307
|
+
this.logger.error(`Slack hook error: ${errorMessage}`);
|
|
308
|
+
return {
|
|
309
|
+
success: false,
|
|
310
|
+
hookType: "slack",
|
|
311
|
+
durationMs,
|
|
312
|
+
error: errorMessage,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
catch (error) {
|
|
317
|
+
const durationMs = Date.now() - startTime;
|
|
318
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
319
|
+
this.logger.error(`Slack hook error: ${errorMessage}`);
|
|
320
|
+
return {
|
|
321
|
+
success: false,
|
|
322
|
+
hookType: "slack",
|
|
323
|
+
durationMs,
|
|
324
|
+
error: errorMessage,
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
//# sourceMappingURL=slack.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack.js","sourceRoot":"","sources":["../../../src/hooks/runners/slack.ts"],"names":[],"mappings":"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"}
|
package/dist/hooks/types.d.ts
CHANGED
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
* are defined in config/schema.ts and exported from config/index.ts.
|
|
9
9
|
* This file contains only runtime types not derived from Zod schemas.
|
|
10
10
|
*/
|
|
11
|
-
import type { HookEvent, HookConfig, ShellHookConfig, WebhookHookConfig, DiscordHookConfig, AgentHooks, ShellHookConfigInput, WebhookHookConfigInput, DiscordHookConfigInput, HookConfigInput } from "../config/schema.js";
|
|
12
|
-
export type { HookEvent, HookConfig, ShellHookConfig, WebhookHookConfig, DiscordHookConfig };
|
|
13
|
-
export type { ShellHookConfigInput, WebhookHookConfigInput, DiscordHookConfigInput, HookConfigInput };
|
|
11
|
+
import type { HookEvent, HookConfig, ShellHookConfig, WebhookHookConfig, DiscordHookConfig, SlackHookConfig, AgentHooks, ShellHookConfigInput, WebhookHookConfigInput, DiscordHookConfigInput, SlackHookConfigInput, HookConfigInput } from "../config/schema.js";
|
|
12
|
+
export type { HookEvent, HookConfig, ShellHookConfig, WebhookHookConfig, DiscordHookConfig, SlackHookConfig };
|
|
13
|
+
export type { ShellHookConfigInput, WebhookHookConfigInput, DiscordHookConfigInput, SlackHookConfigInput, HookConfigInput };
|
|
14
14
|
/**
|
|
15
15
|
* Context payload passed to all hooks
|
|
16
16
|
*
|
|
@@ -136,7 +136,7 @@ export interface HookResult {
|
|
|
136
136
|
/**
|
|
137
137
|
* Hook type that was executed
|
|
138
138
|
*/
|
|
139
|
-
hookType: "shell" | "webhook" | "discord";
|
|
139
|
+
hookType: "shell" | "webhook" | "discord" | "slack";
|
|
140
140
|
/**
|
|
141
141
|
* Duration of hook execution in milliseconds
|
|
142
142
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,UAAU,EAEV,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,EACtB,eAAe,EAChB,MAAM,qBAAqB,CAAC;AAI7B,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/hooks/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EACV,SAAS,EACT,UAAU,EACV,eAAe,EACf,iBAAiB,EACjB,iBAAiB,EACjB,eAAe,EACf,UAAU,EAEV,oBAAoB,EACpB,sBAAsB,EACtB,sBAAsB,EACtB,oBAAoB,EACpB,eAAe,EAChB,MAAM,qBAAqB,CAAC;AAI7B,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,eAAe,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,eAAe,EAAE,CAAC;AAC9G,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,oBAAoB,EAAE,eAAe,EAAE,CAAC;AAM5H;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,WAAW,WAAW;IAC1B;;OAEG;IACH,KAAK,EAAE,SAAS,CAAC;IAEjB;;OAEG;IACH,GAAG,EAAE;QACH;;WAEG;QACH,EAAE,EAAE,MAAM,CAAC;QAEX;;WAEG;QACH,OAAO,EAAE,MAAM,CAAC;QAEhB;;WAEG;QACH,YAAY,CAAC,EAAE,MAAM,CAAC;QAEtB;;WAEG;QACH,SAAS,EAAE,MAAM,CAAC;QAElB;;WAEG;QACH,WAAW,EAAE,MAAM,CAAC;QAEpB;;WAEG;QACH,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC;IAEF;;OAEG;IACH,MAAM,EAAE;QACN;;WAEG;QACH,OAAO,EAAE,OAAO,CAAC;QAEjB;;WAEG;QACH,MAAM,EAAE,MAAM,CAAC;QAEf;;WAEG;QACH,KAAK,CAAC,EAAE,MAAM,CAAC;KAChB,CAAC;IAEF;;OAEG;IACH,KAAK,EAAE;QACL;;WAEG;QACH,EAAE,EAAE,MAAM,CAAC;QAEX;;WAEG;QACH,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IAEF;;;;;;;;;;;;;;;;;OAiBG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAMD;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;OAEG;IACH,OAAO,EAAE,OAAO,CAAC;IAEjB;;OAEG;IACH,QAAQ,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;IAEpD;;OAEG;IACH,UAAU,EAAE,MAAM,CAAC;IAEnB;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IAEf;;OAEG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAMD;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,EAAE,CAAC;CACzB;AAMD;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG,UAAU,CAAC;AAM1C;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB;;;;;;OAMG;IACH,OAAO,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;CACxE"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-sender-mcp.test.d.ts","sourceRoot":"","sources":["../../../src/runner/__tests__/file-sender-mcp.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { resolve, join } from "node:path";
|
|
3
|
+
// Mock node:fs/promises
|
|
4
|
+
vi.mock("node:fs/promises", () => ({
|
|
5
|
+
readFile: vi.fn(),
|
|
6
|
+
realpath: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
import { createFileSenderDef } from "../file-sender-mcp.js";
|
|
9
|
+
import { readFile, realpath } from "node:fs/promises";
|
|
10
|
+
// =============================================================================
|
|
11
|
+
// Helpers
|
|
12
|
+
// =============================================================================
|
|
13
|
+
function createTestContext(overrides = {}) {
|
|
14
|
+
return {
|
|
15
|
+
workingDirectory: "/workspace",
|
|
16
|
+
uploadFile: vi.fn().mockResolvedValue({ fileId: "F12345" }),
|
|
17
|
+
...overrides,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Extract the tool handler from the def's first tool.
|
|
22
|
+
*/
|
|
23
|
+
function getToolHandler(context) {
|
|
24
|
+
const def = createFileSenderDef(context);
|
|
25
|
+
return def.tools[0].handler;
|
|
26
|
+
}
|
|
27
|
+
// =============================================================================
|
|
28
|
+
// Tests
|
|
29
|
+
// =============================================================================
|
|
30
|
+
describe("createFileSenderDef", () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
33
|
+
});
|
|
34
|
+
it("returns a def with the correct server name", () => {
|
|
35
|
+
const context = createTestContext();
|
|
36
|
+
const def = createFileSenderDef(context);
|
|
37
|
+
expect(def.name).toBe("herdctl-file-sender");
|
|
38
|
+
});
|
|
39
|
+
it("defines a single herdctl_send_file tool", () => {
|
|
40
|
+
const context = createTestContext();
|
|
41
|
+
const def = createFileSenderDef(context);
|
|
42
|
+
expect(def.tools).toHaveLength(1);
|
|
43
|
+
expect(def.tools[0].name).toBe("herdctl_send_file");
|
|
44
|
+
});
|
|
45
|
+
it("includes file_path as a required property in the input schema", () => {
|
|
46
|
+
const context = createTestContext();
|
|
47
|
+
const def = createFileSenderDef(context);
|
|
48
|
+
const schema = def.tools[0].inputSchema;
|
|
49
|
+
expect(schema.required).toContain("file_path");
|
|
50
|
+
expect(schema.properties).toHaveProperty("file_path");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe("herdctl_send_file tool handler", () => {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
vi.clearAllMocks();
|
|
56
|
+
// By default, realpath acts as identity (no symlinks)
|
|
57
|
+
vi.mocked(realpath).mockImplementation(async (p) => String(p));
|
|
58
|
+
});
|
|
59
|
+
it("uploads a file within the working directory", async () => {
|
|
60
|
+
const context = createTestContext();
|
|
61
|
+
const handler = getToolHandler(context);
|
|
62
|
+
const mockBuffer = Buffer.from("test content");
|
|
63
|
+
vi.mocked(readFile).mockResolvedValue(mockBuffer);
|
|
64
|
+
const result = await handler({ file_path: "report.pdf" });
|
|
65
|
+
expect(readFile).toHaveBeenCalledWith(resolve("/workspace", "report.pdf"));
|
|
66
|
+
expect(context.uploadFile).toHaveBeenCalledWith({
|
|
67
|
+
fileBuffer: mockBuffer,
|
|
68
|
+
filename: "report.pdf",
|
|
69
|
+
message: undefined,
|
|
70
|
+
});
|
|
71
|
+
expect(result.isError).toBeUndefined();
|
|
72
|
+
expect(result.content[0].text).toContain("uploaded successfully");
|
|
73
|
+
expect(result.content[0].text).toContain("F12345");
|
|
74
|
+
});
|
|
75
|
+
it("passes optional message to uploadFile", async () => {
|
|
76
|
+
const context = createTestContext();
|
|
77
|
+
const handler = getToolHandler(context);
|
|
78
|
+
vi.mocked(readFile).mockResolvedValue(Buffer.from("data"));
|
|
79
|
+
await handler({
|
|
80
|
+
file_path: "output.csv",
|
|
81
|
+
message: "Here is the CSV export",
|
|
82
|
+
});
|
|
83
|
+
expect(context.uploadFile).toHaveBeenCalledWith(expect.objectContaining({
|
|
84
|
+
message: "Here is the CSV export",
|
|
85
|
+
}));
|
|
86
|
+
});
|
|
87
|
+
it("uses filename override when provided", async () => {
|
|
88
|
+
const context = createTestContext();
|
|
89
|
+
const handler = getToolHandler(context);
|
|
90
|
+
vi.mocked(readFile).mockResolvedValue(Buffer.from("data"));
|
|
91
|
+
await handler({
|
|
92
|
+
file_path: "tmp/abc123.pdf",
|
|
93
|
+
filename: "quarterly-report.pdf",
|
|
94
|
+
});
|
|
95
|
+
expect(context.uploadFile).toHaveBeenCalledWith(expect.objectContaining({
|
|
96
|
+
filename: "quarterly-report.pdf",
|
|
97
|
+
}));
|
|
98
|
+
});
|
|
99
|
+
it("rejects paths that escape the working directory with ../", async () => {
|
|
100
|
+
const context = createTestContext();
|
|
101
|
+
const handler = getToolHandler(context);
|
|
102
|
+
const result = await handler({
|
|
103
|
+
file_path: "../../../etc/passwd",
|
|
104
|
+
});
|
|
105
|
+
expect(result.isError).toBe(true);
|
|
106
|
+
expect(result.content[0].text).toContain("escapes working directory");
|
|
107
|
+
expect(context.uploadFile).not.toHaveBeenCalled();
|
|
108
|
+
expect(readFile).not.toHaveBeenCalled();
|
|
109
|
+
});
|
|
110
|
+
it("rejects absolute paths outside working directory", async () => {
|
|
111
|
+
const context = createTestContext();
|
|
112
|
+
const handler = getToolHandler(context);
|
|
113
|
+
const result = await handler({
|
|
114
|
+
file_path: "/etc/passwd",
|
|
115
|
+
});
|
|
116
|
+
expect(result.isError).toBe(true);
|
|
117
|
+
expect(result.content[0].text).toContain("escapes working directory");
|
|
118
|
+
expect(context.uploadFile).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
it("rejects symlinks that resolve outside the working directory", async () => {
|
|
121
|
+
const context = createTestContext();
|
|
122
|
+
const handler = getToolHandler(context);
|
|
123
|
+
// Simulate a symlink: /workspace/link.txt -> /etc/passwd
|
|
124
|
+
vi.mocked(realpath).mockImplementation(async (p) => {
|
|
125
|
+
const s = String(p);
|
|
126
|
+
if (s === resolve("/workspace", "link.txt"))
|
|
127
|
+
return "/etc/passwd";
|
|
128
|
+
return s;
|
|
129
|
+
});
|
|
130
|
+
const result = await handler({ file_path: "link.txt" });
|
|
131
|
+
expect(result.isError).toBe(true);
|
|
132
|
+
expect(result.content[0].text).toContain("escapes working directory");
|
|
133
|
+
expect(context.uploadFile).not.toHaveBeenCalled();
|
|
134
|
+
expect(readFile).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
it("allows absolute paths within the working directory", async () => {
|
|
137
|
+
const context = createTestContext();
|
|
138
|
+
const handler = getToolHandler(context);
|
|
139
|
+
vi.mocked(readFile).mockResolvedValue(Buffer.from("data"));
|
|
140
|
+
const result = await handler({
|
|
141
|
+
file_path: "/workspace/subdir/file.txt",
|
|
142
|
+
});
|
|
143
|
+
expect(result.isError).toBeUndefined();
|
|
144
|
+
expect(context.uploadFile).toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
it("allows nested relative paths within working directory", async () => {
|
|
147
|
+
const context = createTestContext();
|
|
148
|
+
const handler = getToolHandler(context);
|
|
149
|
+
vi.mocked(readFile).mockResolvedValue(Buffer.from("data"));
|
|
150
|
+
const result = await handler({
|
|
151
|
+
file_path: "subdir/deep/file.txt",
|
|
152
|
+
});
|
|
153
|
+
expect(result.isError).toBeUndefined();
|
|
154
|
+
expect(readFile).toHaveBeenCalledWith(join("/workspace", "subdir/deep/file.txt"));
|
|
155
|
+
});
|
|
156
|
+
it("returns error when file does not exist (realpath ENOENT)", async () => {
|
|
157
|
+
const context = createTestContext();
|
|
158
|
+
const handler = getToolHandler(context);
|
|
159
|
+
vi.mocked(realpath).mockRejectedValue(new Error("ENOENT: no such file or directory"));
|
|
160
|
+
const result = await handler({ file_path: "nonexistent.pdf" });
|
|
161
|
+
expect(result.isError).toBe(true);
|
|
162
|
+
expect(result.content[0].text).toContain("file not found");
|
|
163
|
+
expect(readFile).not.toHaveBeenCalled();
|
|
164
|
+
});
|
|
165
|
+
it("returns error when upload fails", async () => {
|
|
166
|
+
const uploadFile = vi
|
|
167
|
+
.fn()
|
|
168
|
+
.mockRejectedValue(new Error("Slack API error: file_too_large"));
|
|
169
|
+
const context = createTestContext({ uploadFile });
|
|
170
|
+
const handler = getToolHandler(context);
|
|
171
|
+
vi.mocked(readFile).mockResolvedValue(Buffer.from("data"));
|
|
172
|
+
const result = await handler({ file_path: "huge-file.zip" });
|
|
173
|
+
expect(result.isError).toBe(true);
|
|
174
|
+
expect(result.content[0].text).toContain("file_too_large");
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
//# sourceMappingURL=file-sender-mcp.test.js.map
|