Skip to content

feat: Add HTTP trigger alongside Discord — unified broker for scheduler + Discord #99

@howie

Description

@howie

Summary

OpenAB is great as a Discord-to-Claude bridge, but there's a broader use case: using OpenAB as a unified agent broker for both human (Discord) and machine (scheduler/CI) triggers, sharing the same session pool and ACP pipeline.

This issue proposes adding an HTTP trigger option so that automated tasks (cron jobs, schedulers, CI pipelines) can invoke the same agent backend that Discord users interact with — without needing Discord as a dependency.


Motivation

Current state

Discord @mention ──▶ OpenAB ──▶ ACP stdio ──▶ Claude Code / Kiro / Codex

Works perfectly for human-driven interactions. But for automated tasks (e.g. "generate a daily newsletter digest at 07:12 every morning"), Discord is an awkward dependency:

  • Scheduler must send a Discord message and poll the thread for completion
  • No structured return value (exit code, output text)
  • Internet dependency for what is essentially a local task

Proposed state

Discord @mention ──┐
                   ├──▶ OpenAB ──▶ ACP stdio ──▶ Claude Code / Kiro / Codex
HTTP POST /v1/prompt ──┘  (shared session pool)

Both triggers share the same session pool and ACP backend. HTTP callers get a synchronous JSON response; Discord users get the existing streaming thread experience.


Proposed Design

New [http] config block

[http]
enabled = true
port = 7865
bind = "127.0.0.1"          # localhost-only by default
token = "${ACP_HTTP_TOKEN}"  # Bearer token auth (64-char hex)
timeout_ms = 300000

New endpoint: POST /v1/prompt

Request:

{
  "prompt": "Generate a summary of today's newsletters",
  "timeout_ms": 300000,
  "caller": "my-scheduler/v1"
}

Response:

{
  "output": "## Daily Digest\n...",
  "success": true,
  "duration_ms": 8432,
  "timed_out": false
}

Auth: Authorization: Bearer <token>

Health check: GET /health (no auth required)

Implementation sketch

// src/http.rs (new file, ~80 lines)
use axum::{routing::{get, post}, Router, Json, extract::State};
use crate::acp::pool::SessionPool;

async fn handle_prompt(
    State(pool): State<Arc<SessionPool>>,
    headers: HeaderMap,
    Json(req): Json<PromptRequest>,
) -> Json<PromptResponse> {
    // 1. verify Bearer token
    // 2. acquire session from pool (or create ephemeral one)
    // 3. submit prompt via ACP stdio
    // 4. collect response until "done" event
    // 5. return JSON
}

pub fn router(pool: Arc<SessionPool>, token: String) -> Router {
    Router::new()
        .route("/v1/prompt", post(handle_prompt))
        .route("/health", get(health))
        .with_state(pool)
}
// src/main.rs — spawn HTTP server alongside Discord gateway
if config.http.enabled {
    let http_router = http::router(pool.clone(), config.http.token.clone());
    tokio::spawn(axum::serve(
        TcpListener::bind(format!("{}:{}", config.http.bind, config.http.port)).await?,
        http_router,
    ));
}

Additional use case: SSH/OpenShell backend

For users running OpenAB locally (Mac mini, home server) with OpenShell sandboxes, it would be useful to support an SSH-based backend in addition to direct subprocess spawning:

[agent]
command = "ssh"
args = ["openshell-claude-dev", "claude-agent-acp"]
working_dir = "/sandbox"

OpenShell auto-manages SSH config (~/.ssh/config), so no key management is needed. This gives sandboxed execution (Landlock filesystem + network policy) without changing OpenAB's ACP stdio protocol.


Benefits

Current (Discord only) With HTTP trigger
Scheduler/CI invocation ❌ Need Discord message + polling POST /v1/prompt
Return value ❌ Parse Discord thread ✅ Structured JSON
Internet dependency ✅ Required ✅ HTTP works localhost-only
Multi-turn (human) ✅ Discord threads ✅ Unchanged
Session pool sharing N/A ✅ Both triggers share pool
Auth Discord OAuth Bearer token (localhost)

Scope

This is intentionally minimal:

  • New file: src/http.rs (~80 lines)
  • Modified: src/config.rs (add [http] block), src/main.rs (spawn HTTP server)
  • No changes to existing Discord behavior
  • No new dependencies beyond axum (already common in Rust ecosystem)

Questions for maintainers

  1. Is this direction aligned with OpenAB's goals? (universal agent broker vs. Discord-specific tool)
  2. Would you prefer a separate crate/binary for the HTTP server, or integrated?
  3. Any concerns about the Bearer token auth model for localhost use?

Happy to submit a PR if the direction looks good.

Metadata

Metadata

Assignees

No one assigned

    Labels

    featurep2Medium — planned work

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions