@herdctl/core 4.0.0 → 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.
Files changed (106) hide show
  1. package/dist/config/index.d.ts +1 -1
  2. package/dist/config/index.d.ts.map +1 -1
  3. package/dist/config/index.js +3 -1
  4. package/dist/config/index.js.map +1 -1
  5. package/dist/config/schema.d.ts +571 -0
  6. package/dist/config/schema.d.ts.map +1 -1
  7. package/dist/config/schema.js +67 -1
  8. package/dist/config/schema.js.map +1 -1
  9. package/dist/fleet-manager/__tests__/slack-manager.test.d.ts +11 -0
  10. package/dist/fleet-manager/__tests__/slack-manager.test.d.ts.map +1 -0
  11. package/dist/fleet-manager/__tests__/slack-manager.test.js +1022 -0
  12. package/dist/fleet-manager/__tests__/slack-manager.test.js.map +1 -0
  13. package/dist/fleet-manager/context.d.ts +4 -0
  14. package/dist/fleet-manager/context.d.ts.map +1 -1
  15. package/dist/fleet-manager/event-types.d.ts +113 -0
  16. package/dist/fleet-manager/event-types.d.ts.map +1 -1
  17. package/dist/fleet-manager/fleet-manager.d.ts +3 -0
  18. package/dist/fleet-manager/fleet-manager.d.ts.map +1 -1
  19. package/dist/fleet-manager/fleet-manager.js +10 -0
  20. package/dist/fleet-manager/fleet-manager.js.map +1 -1
  21. package/dist/fleet-manager/job-control.d.ts.map +1 -1
  22. package/dist/fleet-manager/job-control.js +5 -2
  23. package/dist/fleet-manager/job-control.js.map +1 -1
  24. package/dist/fleet-manager/slack-manager.d.ts +158 -0
  25. package/dist/fleet-manager/slack-manager.d.ts.map +1 -0
  26. package/dist/fleet-manager/slack-manager.js +570 -0
  27. package/dist/fleet-manager/slack-manager.js.map +1 -0
  28. package/dist/fleet-manager/status-queries.d.ts +2 -1
  29. package/dist/fleet-manager/status-queries.d.ts.map +1 -1
  30. package/dist/fleet-manager/status-queries.js +42 -3
  31. package/dist/fleet-manager/status-queries.js.map +1 -1
  32. package/dist/fleet-manager/types.d.ts +43 -3
  33. package/dist/fleet-manager/types.d.ts.map +1 -1
  34. package/dist/hooks/__tests__/slack-runner.test.d.ts +5 -0
  35. package/dist/hooks/__tests__/slack-runner.test.d.ts.map +1 -0
  36. package/dist/hooks/__tests__/slack-runner.test.js +307 -0
  37. package/dist/hooks/__tests__/slack-runner.test.js.map +1 -0
  38. package/dist/hooks/hook-executor.d.ts +1 -0
  39. package/dist/hooks/hook-executor.d.ts.map +1 -1
  40. package/dist/hooks/hook-executor.js +8 -0
  41. package/dist/hooks/hook-executor.js.map +1 -1
  42. package/dist/hooks/index.d.ts +2 -1
  43. package/dist/hooks/index.d.ts.map +1 -1
  44. package/dist/hooks/index.js +2 -0
  45. package/dist/hooks/index.js.map +1 -1
  46. package/dist/hooks/runners/slack.d.ts +62 -0
  47. package/dist/hooks/runners/slack.d.ts.map +1 -0
  48. package/dist/hooks/runners/slack.js +329 -0
  49. package/dist/hooks/runners/slack.js.map +1 -0
  50. package/dist/hooks/types.d.ts +4 -4
  51. package/dist/hooks/types.d.ts.map +1 -1
  52. package/dist/runner/__tests__/file-sender-mcp.test.d.ts +2 -0
  53. package/dist/runner/__tests__/file-sender-mcp.test.d.ts.map +1 -0
  54. package/dist/runner/__tests__/file-sender-mcp.test.js +177 -0
  55. package/dist/runner/__tests__/file-sender-mcp.test.js.map +1 -0
  56. package/dist/runner/__tests__/job-executor.test.js +12 -12
  57. package/dist/runner/__tests__/job-executor.test.js.map +1 -1
  58. package/dist/runner/file-sender-mcp.d.ts +69 -0
  59. package/dist/runner/file-sender-mcp.d.ts.map +1 -0
  60. package/dist/runner/file-sender-mcp.js +145 -0
  61. package/dist/runner/file-sender-mcp.js.map +1 -0
  62. package/dist/runner/index.d.ts +1 -0
  63. package/dist/runner/index.d.ts.map +1 -1
  64. package/dist/runner/index.js +2 -0
  65. package/dist/runner/index.js.map +1 -1
  66. package/dist/runner/job-executor.d.ts.map +1 -1
  67. package/dist/runner/job-executor.js +35 -5
  68. package/dist/runner/job-executor.js.map +1 -1
  69. package/dist/runner/runtime/__tests__/docker-security.test.js +12 -12
  70. package/dist/runner/runtime/__tests__/docker-security.test.js.map +1 -1
  71. package/dist/runner/runtime/__tests__/mcp-http-bridge.test.d.ts +2 -0
  72. package/dist/runner/runtime/__tests__/mcp-http-bridge.test.d.ts.map +1 -0
  73. package/dist/runner/runtime/__tests__/mcp-http-bridge.test.js +191 -0
  74. package/dist/runner/runtime/__tests__/mcp-http-bridge.test.js.map +1 -0
  75. package/dist/runner/runtime/container-manager.d.ts +5 -1
  76. package/dist/runner/runtime/container-manager.d.ts.map +1 -1
  77. package/dist/runner/runtime/container-manager.js +115 -5
  78. package/dist/runner/runtime/container-manager.js.map +1 -1
  79. package/dist/runner/runtime/container-runner.d.ts +2 -0
  80. package/dist/runner/runtime/container-runner.d.ts.map +1 -1
  81. package/dist/runner/runtime/container-runner.js +121 -74
  82. package/dist/runner/runtime/container-runner.js.map +1 -1
  83. package/dist/runner/runtime/index.d.ts +1 -0
  84. package/dist/runner/runtime/index.d.ts.map +1 -1
  85. package/dist/runner/runtime/index.js +2 -0
  86. package/dist/runner/runtime/index.js.map +1 -1
  87. package/dist/runner/runtime/interface.d.ts +2 -0
  88. package/dist/runner/runtime/interface.d.ts.map +1 -1
  89. package/dist/runner/runtime/mcp-http-bridge.d.ts +39 -0
  90. package/dist/runner/runtime/mcp-http-bridge.d.ts.map +1 -0
  91. package/dist/runner/runtime/mcp-http-bridge.js +205 -0
  92. package/dist/runner/runtime/mcp-http-bridge.js.map +1 -0
  93. package/dist/runner/runtime/sdk-runtime.d.ts.map +1 -1
  94. package/dist/runner/runtime/sdk-runtime.js +74 -1
  95. package/dist/runner/runtime/sdk-runtime.js.map +1 -1
  96. package/dist/runner/types.d.ts +44 -0
  97. package/dist/runner/types.d.ts.map +1 -1
  98. package/dist/state/index.d.ts +1 -1
  99. package/dist/state/index.d.ts.map +1 -1
  100. package/dist/state/index.js +1 -1
  101. package/dist/state/index.js.map +1 -1
  102. package/dist/state/session-validation.d.ts +8 -0
  103. package/dist/state/session-validation.d.ts.map +1 -1
  104. package/dist/state/session-validation.js +36 -0
  105. package/dist/state/session-validation.js.map +1 -1
  106. package/package.json +1 -9
@@ -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"}
@@ -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
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAkBH,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"}
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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"}
@@ -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;AAC7F,YAAY,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,eAAe,EAAE,CAAC;AAMtG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;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,CAAC;IAE1C;;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"}
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=file-sender-mcp.test.d.ts.map
@@ -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