Discourse AI のアセットキー値更新に対する webhook/イベントサポートの追加(または管理者によるサンドボックス化の無効化)

AI アーティファクトの保存データが更新された際に、AI アーティファクトがウェブフックを送信できるようにすることを提案したいです。これには多くの利点がありますが、できるだけ簡潔に説明します。

Discourse を利用している多くの組織は、さまざまな自動化ツールも利用しています。これらの自動化ツールは、ますます人気を集め、セットアップが容易になり、初心者にも扱いやすくなっています。AI アーティファクトには貴重なデータが含まれており、これらを自動化で非常に効果的に活用できます。さらに、データがすでに JSON 形式であるという追加のメリットもあります。

私のケースでは、Docker Compose 環境で Discourse コンテナの隣に n8n を使用しており、Docker ネットワークを介して Discourse インスタンス上のさまざまなタスクを自動化しています。しかし、私が管理している Discourse インスタンスの一つは教育機関/企業向けのもので、AI アーティファクトは学習者のノートや授業ジャーナルなどの追跡に使用されています。このようなデータは、n8n などの自動化ツールを通じて、教師のワークフローの改善や要約作成などに非常に役立ちます。

アーティファクトの更新をポーリングすることで技術的には可能ですが、システムリソースをすぐに圧迫する恐れがあり、せいぜいリソースの無駄遣いになります。また、多くの管理者が持っていない高度な技術スキルを必要とします。

言うまでもなく、これは Discourse のエンタープライズクライアントにとっても非常に有益です。

回避策として、アーティファクトの JavaScript が window.discourseArtifact.set(...) を呼び出した後に外部のウェブフックを呼び出す方法が考えられます。

しかし、これにはいくつかの制限があります:

  1. ブラウザ側での実行であるため、権限のある情報源ではありません。

  2. ユーザーのブラウザでアーティファクトの JavaScript が正常に実行された場合のみ発火します。

  3. CORS、ブラウザのプライバシーツール、ネットワークブロッカー、サンドボックスの制約などの影響を受けます。

  4. サーバーサイドの検証が追加されていない限り、ペイロードが Discourse から送信されたものであるとは信頼できません。

  5. 秘密鍵をアーティファクトの JavaScript に安全に埋め込むことはできません。

サーバーサイドのイベント/ウェブフックの方がはるかに信頼性が高く、安全です。

[details=“私は Ruby 開発者ではないため、GPT-5.5 とこの件について話し合ってきました。彼からも興味深い洞察を得ています…”]
Discourse の現在のウェブフックアーキテクチャに基づくと、クリーンな実装はおそらく AI アーティファクト内に「この URL を呼び出す」という独自機能を提供することではありません。通常の Discourse ウェブフックイベントタイプとして実装し、通常の内部 DiscourseEvent をバックエンドとして使用するべきです。

最適な実装形態

Discourse 開発者は、これを 2 つのレイヤーで実装することを提案します:

  1. アーティファクトの KV レコードが変更されたときに発行される内部イベント

  2. これらの内部イベントを購読し、既存のウェブフック配信システムを使用するウェブフックイベントタイプ

これにより、Discourse の他の部分との一貫性が保たれます。既存のウェブフックは、内部の DiscourseEventconfig/initializers/012-web_hook_events.rb 内の WebHook.enqueue_* 呼び出しにマッピングすることで機能しています。例えば、トピック、投稿、ユーザー、カテゴリ、タグ、レビュー可能なアイテム、通知、いいねイベントなどはこのように接続されています。

アーティファクトストレージパスはシンプルです:ArtifactKeyValuesController#set はキー値レコードを検索または初期化し、key/value/public を割り当てて保存します。destroy はキーで現在のユーザーのレコードを検索して削除します。モデル自体は AiArtifactKeyValue で、アーティファクトとユーザーに属し、keyvaluepublic を持ち、アーティファクト/ユーザー/キーの一意性を強制します。

提案されるイベント名

おそらく 3 つの特定のウェブフックイベントを使用します:

ai_artifact_key_value_created
ai_artifact_key_value_updated
ai_artifact_key_value_deleted

あるいは、単一のイベントでも機能します:

ai_artifact_key_value_changed

…しかし、3 つのイベントの方が Discourse の既存のスタイルに合っています。Discourse にはすでに post_createdpost_editedpost_destroyedcalendar_event_createdcalendar_event_updated など、個別のウェブフックイベント名があります。

関連する可能性が高いファイル/クラス

1. 新しいウェブフックイベントタイプの追加

WebHookEventType は現在、数値定数、group 列挙型、イベント名から ID への TYPES ハッシュを定義しています。

以下のようなものを追加できます:

AI_ARTIFACT = 20

enum :group,
  {
    # 既存のグループ...
    ai_artifact: 18,
  },
  scopes: false

TYPES = {
  # 既存のタイプ...
  ai_artifact_key_value_created: 2001,
  ai_artifact_key_value_updated: 2002,
  ai_artifact_key_value_deleted: 2003,
}

正確な ID は Discourse 管理者次第ですが、競合を避ける必要があります。

また、既存のウェブフックイベントタイプは db/fixtures/007_web_hook_event_types.rb で ID、名前、グループとともにシードされているため、ここにシードエントリを追加する必要があります。

例:

WebHookEventType.seed do |b|
  b.id = WebHookEventType::TYPES[:ai_artifact_key_value_created]
  b.name = "ai_artifact_key_value_created"
  b.group = WebHookEventType.groups[:ai_artifact]
end

WebHookEventType.seed do |b|
  b.id = WebHookEventType::TYPES[:ai_artifact_key_value_updated]
  b.name = "ai_artifact_key_value_updated"
  b.group = WebHookEventType.groups[:ai_artifact]
end

WebHookEventType.seed do |b|
  b.id = WebHookEventType::TYPES[:ai_artifact_key_value_deleted]
  b.name = "ai_artifact_key_value_deleted"
  b.group = WebHookEventType.groups[:ai_artifact]
end

管理画面のウェブフック UI は、Admin::WebHooksController#index がすでにグループ化されたアクティブなイベントタイプを UI 用にシリアライズしているため、これらを自動的に取得するはずです。イベントタイプシリアライザーはすでに idnamegroup を公開しています。

2. Discourse AI が無効な場合にイベントを非表示にする

WebHookEventType.active は、解決済み、割り当て、トピック投票、チャット、カレンダーイベントなど、機能が無効になっている場合にプラグイン依存のウェブフックイベントをすでに非表示にしています。

したがって、以下のような処理を追加できます:

unless defined?(SiteSetting.discourse_ai_enabled) && SiteSetting.discourse_ai_enabled
  ids_to_exclude.concat(
    [
      TYPES[:ai_artifact_key_value_created],
      TYPES[:ai_artifact_key_value_updated],
      TYPES[:ai_artifact_key_value_deleted],
    ],
  )
end

また、アーティファクト固有の設定が存在する場合、または追加された場合は、それを確認することも検討してください。

3. コントローラーだけでなく、モデルから内部イベントを発行する

現在の書き込みパスはコントローラーベースですが、堅牢な場所はコミットコールバックを使用したモデルである可能性が高いです:

class AiArtifactKeyValue < ActiveRecord::Base
  after_create_commit :trigger_created_event
  after_update_commit :trigger_updated_event
  after_destroy_commit :trigger_deleted_event

  private

  def trigger_created_event
    DiscourseEvent.trigger(:ai_artifact_key_value_created, self)
  end

  def trigger_updated_event
    return if previous_changes.slice("value", "public").blank?

    DiscourseEvent.trigger(:ai_artifact_key_value_updated, self)
  end

  def trigger_deleted_event
    DiscourseEvent.trigger(:ai_artifact_key_value_deleted, self)
  end
end

モデルレベルのコールバックは、ArtifactKeyValuesController#set だけでなく、将来の書き込みパスも捕捉します。after_commit 部分が重要なのは、ウェブフック配信がデータベースの変更が安全にコミットされた後にのみキューに追加されるべきだからです。

微妙な点として、アーティファクトが同じ値で set() を呼び出し、実際に変更がない場合に「updated」イベントを発行しないようにする必要があります。previous_changes を確認することで、ノイズの多いウェブフックを回避できます。

4. ウェブフックペイロードシリアライザーの追加

Discourse のウェブフックはシリアライザーを通じてペイロードを生成します。汎用の WebHook.enqueue_object_hooks メソッドはシリアライザーを受け取り、WebHook.generate_payload はシステムユーザーのガーディアンを使用してオブジェクトをシリアライズします。

以下のようなものを追加できます:

class WebHookAiArtifactKeyValueSerializer < ApplicationSerializer
  attributes :id,
             :ai_artifact_id,
             :post_id,
             :topic_id,
             :user_id,
             :key,
             :public,
             :value_included,
             :created_at,
             :updated_at

  def post_id
    object.ai_artifact.post_id
  end

  def topic_id
    object.ai_artifact.post.topic_id
  end

  def value_included
    false
  end
end

value をデフォルトで含めないことを強くお勧めします。アーティファクトの KV データはユーザーごとに非公開である可能性があり、モデルには public フラグがあります。Discourse が値のサポートを望む場合、以下のように明示的に設定できます:

include_value: false
include_public_values: true
include_private_values: false

しかし、安全な v1 では値を完全に省略するのがよいでしょう。

5. 内部イベントをウェブフック配信に接続する

既存のパターンを反映して、config/initializers/012-web_hook_events.rb にハンドラーを追加できます。

以下のようなものです:

%i[
  ai_artifact_key_value_created
  ai_artifact_key_value_updated
  ai_artifact_key_value_deleted
].each do |event|
  DiscourseEvent.on(event) do |key_value|
    artifact = key_value.ai_artifact
    post = artifact.post
    topic = post.topic

    payload =
      WebHook.generate_payload(
        :ai_artifact_key_value,
        key_value,
        WebHookAiArtifactKeyValueSerializer
      )

    WebHook.enqueue_hooks(
      :ai_artifact_key_value,
      event,
      id: key_value.id,
      category_id: topic&.category_id,
      tag_ids: topic&.tags&.pluck(:id),
      payload: payload
    )
  end
end

これにより、既存のウェブフックジョブパイプラインを再利用できます。WebHook.enqueue_hooks はイベントに対してアクティブなウェブフックを検索し、Jobs::EmitWebHookEvent をキューに追加します。このジョブはすでに配信、リトライ、ログ記録、ヘッダー、署名、管理者の可視性を処理しています。

category_idtag_ids を含めるのは良いアイデアです。既存のウェブフックジョブはカテゴリとタグでフィルタリングできるためです。ウェブフックジョブは送信前にカテゴリとタグの制約を確認しています。AI アーティファクトは投稿に属し、モデルではアーティファクトが投稿に属しているため、カテゴリ/トピックのコンテキストを導出できるはずです。

配信されるウェブフックの例

Discourse のウェブフックボディは event_type をトップレベルのルートとして使用するため、以下のような形になるでしょう:

{
  "ai_artifact_key_value": {
    "id": 456,
    "ai_artifact_id": 123,
    "post_id": 789,
    "topic_id": 321,
    "user_id": 42,
    "key": "score",
    "public": true,
    "value_included": false,
    "created_at": "2026-05-26T12:00:00Z",
    "updated_at": "2026-05-26T12:05:00Z"
  }
}

イベント名は既存のウェブフック配信と一貫性を持たせるため、ヘッダーに含まれます:

X-Discourse-Event-Type: ai_artifact_key_value
X-Discourse-Event: ai_artifact_key_value_updated
X-Discourse-Event-Signature: sha256=...

これらのヘッダーは、EmitWebHookEvent が現在ウェブフックヘッダーを構築する方法と整合しています。

開発者が決定する必要がある事項:

値を含めるべきか?
私の投票:デフォルトでは含めない。公開値のみを許可するか、値の含めるかどうかを管理者設定にするのも一案です。プライベートなユーザーごとのアーティファクトデータがサイトから静かに漏れるべきではありません。

イベントは 1 つにするか 3 つにするか?
ウェブフック購読者にとっては 3 つの方がクリーンです。action フィールドを持つ単一のイベントは内部的にはシンプルです。既存の Discourse のスタイルは複数のイベント名を好む傾向があります。

カテゴリ/タグフィルタリングを適用すべきか?
はい、適用すべきだと思います。アーティファクトは投稿/トピックに接続されているため、カテゴリとタグのフィルタは意味を持ちます。

これは Discourse コアで実装すべきか、Discourse AI プラグインで実装すべきか?
データモデルは Discourse AI プラグインにありますが、ウェブフックイベントレジストリはコアにあります。解決済み/チャット/カレンダーなどのバンドルされたプラグインはすでに共有の WebHookEventType リストにウェブフックイベントタイプを持っているため、Discourse は AI アーティファクトイベントもそこに追加することに抵抗がないかもしれません。active メソッドで AI が無効な場合にそれらを非表示にできます。

ウェブフックが存在しない場合でも、内部 DiscourseEvent を発行すべきか?
はい。これにより、ウェブフックとは別に、プラグイン開発者にクリーンなフックポイントが提供されます。

複雑さ

これは中程度だが非常に限定された範囲と捉えています。

おそらく必要な作業は:

  1. 3 つのイベントタイプ定数の追加。

  2. 1 つのウェブフックグループの追加。

  3. 3 つのシードレコードの追加。

  4. 1 つのシリアライザーの追加。

  5. 3 つのモデルコールバックまたはサービスレベルのイベントトリガーの追加。

  6. ウェブフック初期化子ハンドラーの追加。

  7. 作成/更新/削除の仕様の追加。

  8. value を含めるかどうかに関するプライバシー判断の追加。

配信システム、リトライ、署名、イベントログ、ピング/再配信 UI、管理者ウェブフック設定はすでに存在しています。この機能は、アーティファクト KV 変更を既存のシステムに可視化することにあります。

Discourse の自動化における新しいアプローチである Workflows を導入した後に再検討すべき事項。

「いいね!」 2

おお、それは非常に興味深いですね。メタでその情報を見逃しているでしょうか、またタイムラインはありますか?