shell_command_agent.rb 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. module Agents
  2. class ShellCommandAgent < Agent
  3. default_schedule "never"
  4. can_dry_run!
  5. no_bulk_receive!
  6. def self.should_run?
  7. ENV['ENABLE_INSECURE_AGENTS'] == "true"
  8. end
  9. description <<~MD
  10. The Shell Command Agent will execute commands on your local system, returning the output.
  11. `command` specifies the command (either a shell command line string or an array of command line arguments) to be executed, and `path` will tell ShellCommandAgent in what directory to run this command. The content of `stdin` will be fed to the command via the standard input.
  12. `expected_update_period_in_days` is used to determine if the Agent is working.
  13. ShellCommandAgent can also act upon received events. When receiving an event, this Agent's options can interpolate values from the incoming event.
  14. For example, your command could be defined as `{{cmd}}`, in which case the event's `cmd` property would be used.
  15. The resulting event will contain the `command` which was executed, the `path` it was executed under, the `exit_status` of the command, the `errors`, and the actual `output`. ShellCommandAgent will not log an error if the result implies that something went wrong.
  16. If `unbundle` is set to true, the command is run in a clean environment, outside of Huginn's bundler context.
  17. If `suppress_on_failure` is set to true, no event is emitted when `exit_status` is not zero.
  18. If `suppress_on_empty_output` is set to true, no event is emitted when `output` is empty.
  19. *Warning*: This type of Agent runs arbitrary commands on your system, #{Agents::ShellCommandAgent.should_run? ? "but is **currently enabled**" : "and is **currently disabled**"}.
  20. Only enable this Agent if you trust everyone using your Huginn installation.
  21. You can enable this Agent in your .env file by setting `ENABLE_INSECURE_AGENTS` to `true`.
  22. MD
  23. event_description <<~MD
  24. Events look like this:
  25. {
  26. "command": "pwd",
  27. "path": "/home/Huginn",
  28. "exit_status": 0,
  29. "errors": "",
  30. "output": "/home/Huginn"
  31. }
  32. MD
  33. def default_options
  34. {
  35. 'path' => "/",
  36. 'command' => "pwd",
  37. 'unbundle' => false,
  38. 'suppress_on_failure' => false,
  39. 'suppress_on_empty_output' => false,
  40. 'expected_update_period_in_days' => 1
  41. }
  42. end
  43. def validate_options
  44. unless options['path'].present? && options['command'].present? && options['expected_update_period_in_days'].present?
  45. errors.add(:base, "The path, command, and expected_update_period_in_days fields are all required.")
  46. end
  47. case options['stdin']
  48. when String, nil
  49. else
  50. errors.add(:base, "stdin must be a string.")
  51. end
  52. unless Array(options['command']).all? { |o| o.is_a?(String) }
  53. errors.add(:base, "command must be a shell command line string or an array of command line arguments.")
  54. end
  55. unless File.directory?(interpolated['path'])
  56. errors.add(:base, "#{options['path']} is not a real directory.")
  57. end
  58. end
  59. def working?
  60. Agents::ShellCommandAgent.should_run? && event_created_within?(interpolated['expected_update_period_in_days']) && !recent_error_logs?
  61. end
  62. def receive(incoming_events)
  63. incoming_events.each do |event|
  64. handle(interpolated(event), event)
  65. end
  66. end
  67. def check
  68. handle(interpolated)
  69. end
  70. private
  71. def handle(opts, event = nil)
  72. if Agents::ShellCommandAgent.should_run?
  73. command = opts['command']
  74. path = opts['path']
  75. stdin = opts['stdin']
  76. result, errors, exit_status = run_command(path, command, stdin, **interpolated.slice(:unbundle).symbolize_keys)
  77. payload = {
  78. 'command' => command,
  79. 'path' => path,
  80. 'exit_status' => exit_status,
  81. 'errors' => errors,
  82. 'output' => result,
  83. }
  84. unless suppress_event?(payload)
  85. created_event = create_event(payload:)
  86. end
  87. log("Ran '#{command}' under '#{path}'", outbound_event: created_event, inbound_event: event)
  88. else
  89. log("Unable to run because insecure agents are not enabled. Edit ENABLE_INSECURE_AGENTS in the Huginn .env configuration.")
  90. end
  91. end
  92. def run_command(path, command, stdin, unbundle: false)
  93. if unbundle
  94. return Bundler.with_original_env {
  95. run_command(path, command, stdin)
  96. }
  97. end
  98. begin
  99. rout, wout = IO.pipe
  100. rerr, werr = IO.pipe
  101. rin, win = IO.pipe
  102. pid = spawn(*command, chdir: path, out: wout, err: werr, in: rin)
  103. wout.close
  104. werr.close
  105. rin.close
  106. if stdin
  107. win.write stdin
  108. win.close
  109. end
  110. (result = rout.read).strip!
  111. (errors = rerr.read).strip!
  112. _, status = Process.wait2(pid)
  113. exit_status = status.exitstatus
  114. rescue StandardError => e
  115. errors = e.to_s
  116. result = ''.freeze
  117. exit_status = nil
  118. end
  119. [result, errors, exit_status]
  120. end
  121. def suppress_event?(payload)
  122. (boolify(interpolated['suppress_on_failure']) && payload['exit_status'].nonzero?) ||
  123. (boolify(interpolated['suppress_on_empty_output']) && payload['output'].empty?)
  124. end
  125. end
  126. end