Skip to content

feat(discord): add /auth slash command for device-flow auth#1185

Open
chaodu-agent wants to merge 9 commits into
mainfrom
feat/auth-slash-command
Open

feat(discord): add /auth slash command for device-flow auth#1185
chaodu-agent wants to merge 9 commits into
mainfrom
feat/auth-slash-command

Conversation

@chaodu-agent

@chaodu-agent chaodu-agent commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds /auth Discord slash command that executes $OPENAB_AGENT_AUTH_COMMAND and relays the device-flow URL + code back to the user via ephemeral DM.

Flow Diagram

┌─────────────┐         ┌──────────────┐         ┌────────────────────┐
│  Discord    │         │     OAB      │         │  Auth CLI          │
│  (User DM)  │         │  (Handler)   │         │  (e.g. codex       │
│             │         │              │         │   login --device)  │
└──────┬──────┘         └──────┬───────┘         └─────────┬──────────┘
       │                       │                           │
       │  /auth                │                           │
       ├──────────────────────▶│                           │
       │                       │                           │
       │  [access check]       │                           │
       │  [DM-only check]      │                           │
       │  [single-flight]      │                           │
       │                       │                           │
       │  defer (ephemeral)    │                           │
       │◀──────────────────────┤                           │
       │                       │                           │
       │                       │  sh -c $AUTH_CMD          │
       │                       ├──────────────────────────▶│
       │                       │                           │
       │                       │  stdout: URL + code       │
       │                       │◀──────────────────────────┤
       │                       │                           │
       │  🔐 URL + code        │                           │
       │◀──────────────────────┤                           │
       │                       │                           │
       │                       │      (process blocks,     │
       │  [user opens URL,     │       polling OAuth       │
       │   enters code in      │       server...)          │
       │   browser]            │                           │
       │                       │                           │
       │                       │  exit 0                   │
       │                       │◀──────────────────────────┤
       │                       │                           │
       │  ✅ Authenticated!    │                           │
       │◀──────────────────────┤                           │
       │                       │                           │

Security Controls

/auth invoked
  │
  ├─ is_denied_user? ──▶ 🚫 rejected
  │
  ├─ guild_id.is_some()? ──▶ 🔒 DM-only
  │
  ├─ AUTH_IN_PROGRESS? ──▶ ⚠️ already running
  │
  ├─ OPENAB_AGENT_AUTH_COMMAND unset? ──▶ ⚠️ not configured
  │
  └─ ✅ proceed with deferred ephemeral + spawn

Architecture

  • Drain tasks: Separate tokio::spawn for stdout/stderr → run to EOF (no SIGPIPE)
  • URL detection: tokio::sync::Notify (cancellation-safe)
  • Single-flight: static AtomicBool + Drop guard (panic-safe)
  • DM check: cmd.guild_id.is_some() (local field, no API call)
  • Truncation: chars().take() (UTF-8-safe, Discord 2000-char limit)
  • Timeout: 14 min (leaves headroom for Discord interaction token TTL)
  • Tracing: info/warn at key lifecycle points

Changes

  • crates/openab-core/src/discord.rs — Register + implement /auth
  • docs/slash-commands.md — Full documentation

Review History

6 commits, 5 review rounds, 3 reviewers (擺渡/口渡/覺渡/普渡), all LGTM ✅

Implements a new /auth Discord slash command that executes the
OPENAB_AGENT_AUTH_COMMAND env var, captures the device flow URL and
code from stdout/stderr, and relays them to the user as an ephemeral
Discord message.

Flow:
1. User runs /auth
2. OAB execs $OPENAB_AGENT_AUTH_COMMAND (e.g. 'codex login --device-auth')
3. Captures URL+code from stdout within 30s
4. Sends ephemeral followup with auth instructions
5. Waits up to 15min for process to exit (user authorizes in browser)
6. Reports success/failure/timeout

Also updates docs/slash-commands.md with /auth documentation.
@chaodu-agent chaodu-agent requested a review from thepagent as a code owner June 24, 2026 00:20
chaodu-agent added 3 commits June 24, 2026 00:23
- Add is_denied_user access control gate (🔴 #1)
- Kill orphaned child process on empty-output early return (🔴 #2)
- Add single-flight AtomicBool guard to prevent concurrent /auth (🟡 #4)
- Truncate output to fit Discord 2000-char limit (🟡 #5)
- Scope readers in a block to drop cleanly before wait (🟡 #6)
Address 擺渡法師 and 口渡法師 review feedback:

- Spawn independent tokio tasks for stdout/stderr draining (run to EOF)
- Fixes SIGPIPE: pipes stay open while child waits for browser auth
- Fixes cancellation safety: no more tokio::select! on next_line()
- Fixes asymmetric drain: both streams captured independently
- Use tokio::sync::Notify for URL detection signaling
- tokio::join! drain tasks before exiting to ensure clean shutdown
- Kill + join on empty-output and timeout paths
- /auth now only works in DMs (rejects guild channels with guidance)
- Truncation uses chars().take() instead of byte slicing (no panic on
  multi-byte UTF-8)
- Budget calculated with chars().count() for prefix/suffix (correct
  for Discord's character-based limit)
- Updated docs to document DM-only requirement
@chaodu-agent

This comment has been minimized.

chaodu-agent added 2 commits June 24, 2026 00:56
- child.wait() timeout reduced from 15min to 14min so the final
  success/failure followup can still be delivered within Discord's
  15min interaction token TTL
- Docs: added 30s URL-collection window, allowed_users requirement,
  and single-flight behavior notes
- Replace to_channel() API call with cmd.guild_id.is_some() for DM check (F1)
- Add AuthGuard Drop impl to reset AUTH_IN_PROGRESS on task panic (F2)
- Add tracing::info/warn at key lifecycle points in spawned task (F3)
- Remove redundant manual store(false) calls now covered by Drop guard
@chaodu-agent

This comment has been minimized.

- Add child.wait() arm to URL-collection select! so a fast-failing auth
  command reports immediately instead of stalling the full 30s window (#1)
- Reject bot users in /auth, consistent with /remind (#8)
- Record invoking user_id in the auth start audit log (#7)
- Truncate output by UTF-16 code units to match Discord's 2000-char limit,
  preventing rejection on non-BMP-heavy output (#5)
- Handle std::sync::Mutex poison in drain/collect paths to avoid panic
  cascade and silent output loss (#9)
- Clarify the 'no output' error message with cause and remedy (#6)
- Fix docs intro contradicting /auth DM-only and mark it DM-only in the
  command table (#3)
@chaodu-agent

Copy link
Copy Markdown
Collaborator Author

CHANGES REQUESTED ⚠️ — Solid, well-hardened follow-up; prior blockers all resolved. Two minor hardening items remain (device-code capture timing + missing tests). Functionally mergeable — final call is the maintainer's.

What This PR Does

Adds a /auth Discord slash command that runs the operator-configured OPENAB_AGENT_AUTH_COMMAND (e.g. an OAuth device-flow login), captures the verification URL + code from its output, and relays them back to the invoking user as an ephemeral DM. This lets a user re-authenticate the backend agent without shell access to the host.

How It Works

  • Gating: bot-user reject → allowlist (is_denied_user) → DM-only (guild_id.is_some()) → process-wide single-flight (AtomicBool + Drop guard) → env-var presence check.
  • Execution: spawns sh -c "$OPENAB_AGENT_AUTH_COMMAND" (no user-controlled input reaches the shell), with dedicated stdout/stderr drain tasks reading to EOF to avoid SIGPIPE/backpressure.
  • URL relay: a tokio::select! waits for the first URL line, an early child exit, or a 30s window; output is truncated to Discord's 2000-char limit (UTF-16 budgeted) and sent ephemerally.
  • Completion: waits up to 14 min (headroom under Discord's interaction-token TTL) for the process to exit, then reports success/failure.

Findings

# Severity Finding Location
1 🟡 Device code may be truncated if printed >500ms after the URL. After the first URL line, only sleep(500ms) elapses before snapshotting output. Most CLIs print URL+code together, but a slow CLI that emits the code later would relay an incomplete payload — defeating the feature's purpose. Consider idle-based or line-count-based capture. discord.rssleep(500ms) after url_found
2 🟡 No regression tests. The non-trivial UTF-16 truncation budgeting and auth state transitions are embedded inline. The repo convention is to extract pure decision helpers (cf. is_denied_user) and unit-test them; the truncation logic in particular is a good candidate. discord.rs/auth handler
3 🟢 Both prior-round 🟠 items addressed: early-exit arm added to the URL select!, and UTF-16-correct truncation.
4 🟢 Robust hardening landed: bot rejection, Mutex poison-tolerance (into_inner), user_id audit log, drain-task join on all exit paths, clearer no-output message, doc header reconciled.
5 🟢 No command injection — only the operator-set env var reaches sh -c. Layered security (allowlist → DM-only → single-flight → env guard), panic-safe Drop guard, ephemeral delivery.
Prior-round resolution detail (head 399288908791dd)
Prior finding Status
🟠 Fast-fail: no child.wait() arm in URL window ✅ Fixed — res = child.wait() arm sets early_exit
🟠 / 🟡 Doc header contradicts DM-only ✅ Fixed — opening line now calls out the /auth exception
🟡 UTF-16 vs scalar truncation ✅ Fixed — encode_utf16().count() + len_utf16() budgeting
🟡 Ambiguous "no output" error ✅ Fixed — message now states cause + remedy
🟡 Audit log missing invoking user ✅ Fixed — info!(user_id, ...)
🟡 No bot rejection (inconsistent w/ other handlers) ✅ Fixed — cmd.user.bot reject added
🟡 std::sync::Mutex poison cascade ✅ Fixed — unwrap_or_else(|e| e.into_inner())
🟡 Timeout path doesn't join drain tasks ✅ Fixed — tokio::join! runs after the match for all paths
🟡 Process-wide single-flight ✅ Documented as intended behavior
🟡 500ms trailing-capture heuristic ⏳ Outstanding (F1 above)
🟡 No regression tests ⏳ Outstanding (F2 above)
CI status
  • check (Rust build/clippy) and the changes gate: pass.
  • 14 of 15 smoke-test matrix variants: pass (built in ~6 min each from identical Rust).
  • 1 smoke-test variant (Dockerfile, kiro-cli) failed at the Build image step in ~25s — a transient/infra failure (image pull/build), not a code defect: the same Rust source compiled and ran across every other variant. Recommend a re-run.
Baseline Check
  • PR opened: 2026-06-24.
  • Main has no /auth command or OPENAB_AGENT_AUTH_COMMAND handling.
  • Net-new value: a gated, DM-only, ephemeral relay of an agent device-flow login — entirely additive (one new command registration + handler + docs).

chaodu-agent added 2 commits June 24, 2026 01:34
…ht flag

Addresses review nit: the AUTH_IN_PROGRESS flag carries no dependent data,
so SeqCst is stronger than necessary. Acquire on the guard acquire (swap)
and Release on each clear (store) is sufficient and clearer in intent.
Round-2 review feedback:
- F1: when the auth command exits 0 during the URL-collection window without
  producing a login URL, show a warning + retry prompt instead of
  '✅ Auth command completed.' (avoids false confidence that auth succeeded)
- F2: document the early-exit-without-URL error case in slash-commands.md
- F3: document bot-user rejection (and DM-only) in the /auth error cases
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant