Skip to content

chore: promote staging to staging-promote/ea150927-24536700939 (2026-04-16 23:13 UTC)#2554

Open
ironclaw-ci[bot] wants to merge 1 commit intostaging-promote/ea150927-24536700939from
staging-promote/3ac8e5f7-24538841293
Open

chore: promote staging to staging-promote/ea150927-24536700939 (2026-04-16 23:13 UTC)#2554
ironclaw-ci[bot] wants to merge 1 commit intostaging-promote/ea150927-24536700939from
staging-promote/3ac8e5f7-24538841293

Conversation

@ironclaw-ci
Copy link
Copy Markdown
Contributor

@ironclaw-ci ironclaw-ci bot commented Apr 16, 2026

Auto-promotion from staging CI

Batch range: a53eac5c2dec6b6cd5c08189086093fde64aa9cb..3ac8e5f7e8ac93f34fcd20227b39b6417fc0a2ba
Promotion branch: staging-promote/3ac8e5f7-24538841293
Base: staging-promote/ea150927-24536700939
Triggered by: Staging CI batch at 2026-04-16 23:13 UTC

Commits in this batch (60):

Current commits in this promotion (1)

Current base: staging-promote/ea150927-24536700939
Current head: staging-promote/3ac8e5f7-24538841293
Current range: origin/staging-promote/ea150927-24536700939..origin/staging-promote/3ac8e5f7-24538841293

Auto-updated by staging promotion metadata workflow

Waiting for gates:

  • Tests: pending
  • E2E: pending
  • Claude Code review: pending (will post comments on this PR)

Auto-created by staging-ci workflow

* fix(channels): wire up pairing approval, polling restart, and onboarding state

The Telegram channel setup flow via the gateway was broken end-to-end.
Four interconnected bugs prevented pairing/ownership from completing:

1. pairing_approve_handler only wrote to channel_identities DB — the
   running WasmChannel's owner_actor_id was never updated, so the
   owner was never recognized and broadcast metadata was never stored.

2. refresh_active_channel() re-ran on_start() but never called
   ensure_polling(), leaving polling in a stale state on repeated
   tool_activate calls and causing Telegram 409 conflicts.

3. activate_wasm_channel() had a TOCTOU race on active_channel_names
   that allowed duplicate polling loops, and hot_add() didn't await
   old polling task termination.

4. onboarding_state was always None in extension API responses and
   PairingRequired SSE was never emitted, so the frontend could
   never render the pairing card.

Changes:
- approve_pairing (DB trait + both backends) now returns external_id
- WasmChannel.owner_actor_id wrapped in RwLock with set_owner_actor_id()
- ExtensionManager.complete_pairing_approval() orchestrates: persist
  owner_id → update running channel → restart polling
- pairing_approve_handler calls complete_pairing_approval and emits
  PairingCompleted SSE (scoped to approving user)
- refresh_active_channel() calls ensure_polling() and syncs owner
- Per-channel activation mutex prevents TOCTOU race
- hot_add() drops write lock before awaiting shutdown
- Extension list handlers populate onboarding_state when Pairing
- derive_onboarding() helper in handlers/extensions.rs
- Regression tests for derive_onboarding and resolve_message_scope

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(bridge): eliminate dual card + text emission for gate-paused flows

When the v2 engine hits a gate-paused state (approval needed, auth
required), the web gateway was sending BOTH an interactive card (via
send_status → SSE) AND a redundant text message (via AppEvent::Response).
Users saw a duplicate prompt.

Root cause: v2 bridge functions returned Ok(Some(text)) for gate-paused
outcomes, which mapped via from_legacy to HandleOutcome::Respond — sending
both the card and the text. The v1 path correctly used HandleOutcome::Pending.

Fix:
- Gate-paused paths in router.rs now return Ok(None) instead of text
- New bridge_to_outcome() checks has_any_pending_gate() after each v2
  bridge call — if a gate exists, returns Pending (suppresses text + Done)
- New from_bridge() maps None → NoResponse (not Shutdown) for v2 paths
- Removed pending_gate_prompt_message() — the function that generated
  the duplicate text
- notify_pending_gate() no longer emits GateRequired SSE directly
  (redundant with send_pending_gate_status per-channel routing)
- Updated 3 tests to assert None return + StatusUpdate delivery

Each channel renders the approval/auth card natively via send_status:
web → SSE card, TUI → widget, relay → buttons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review comments

- bridge_to_outcome: only return Pending when handler returned None
  (preserves legitimate text responses for ambiguous gate messages)
- process_emitted_messages: clone owner_actor_id out of read lock
  before awaiting resolve_message_scope_with_pairing
- Normalize channel_name to lowercase in complete_pairing_approval
  and pairing_approve_handler for consistent webhook/store lookups
- cargo fmt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address self-review — BridgeOutcome enum, ExternalId newtype, pairing extraction

- Replace Option<String> bridge handler returns with typed BridgeOutcome
  enum (Respond/NoResponse/Pending), eliminating post-hoc has_any_pending_gate
  query and the None→NoResponse mapping that swallowed v2 shutdown signals
- Add ExternalId newtype for approve_pairing return (was bare String)
- Fix noop PairingStore::approve to return NotFound instead of Ok("")
- Extract pairing approval orchestration to src/pairing/approval.rs
- Clone RwLock<owner_actor_id> before awaiting in respond()
- Downgrade warn! to debug! in pairing handlers (TUI logging rule)
- Gate TELEGRAM_TEST_API_BASE_ENV const behind cfg(test/debug_assertions)
- Remove hardcoded Telegram auth instructions; use capabilities prompt
- Fix unused mut receiver in test

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(channels): remove dead Telegram verification flow, consolidate to generic pairing

The Telegram-specific verification challenge (/start CODE deep link flow)
blocked the generic pairing flow from ever running — configure() returned
early with activated:false when the challenge was pending, so the channel
never started polling and users couldn't generate pairing codes.

Removed ~1200 lines:
- TelegramBindingResult, TelegramBindingData, TelegramOwnerBindingState,
  TelegramVerificationMeta, PendingTelegramVerificationChallenge types
- configure_telegram_binding, resolve_telegram_binding,
  issue_telegram_verification_challenge, notify_telegram_owner_verified
  and all Telegram API response types (getUpdates polling loop, etc.)
- ConfigureResult.verification field + VerificationChallenge re-export
- All verification-related test fixtures and 6 test functions
- Dead RecordingChannel test helper, unused set_channel_owner_id method
- Gated send_telegram_text_message + helpers behind cfg(test)

Replaced with:
- validate_telegram_token() — lightweight getMe call for token validation
  + bot_username extraction (persisted for mention detection)
- All channels now follow: credentials → validate → activate → pairing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(channels): broadcast PairingRequired SSE after activation in pairing mode

After a channel activates with no owner binding, broadcast a per-user
PairingRequired SSE event so the web UI shows the pairing card without
requiring a manual refresh. Also populate pairing_required, onboarding_state,
and onboarding fields on ConfigureResult so callers know the channel
needs pairing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(agent): don't persist auth instructions as turn response

When a tool triggers an auth gate (awaiting_token), the dispatcher
already sends an AuthRequired card and puts the thread in auth mode.
The thread_ops handler was then calling complete_turn(&instructions)
which overwrote auth mode back to Idle AND persisted the auth prompt
("Enter your Telegram Bot API token...") as the turn response — rendering
a redundant text bubble alongside the auth card.

Fix: skip complete_turn and persist_assistant_response for AuthPending.
The turn is paused (not complete), and the auth card is the only
user-facing signal. Tool calls are still persisted for history.

Also removes the now-unused `instructions` field from
AgenticLoopResult::AuthPending — the instructions were already sent
via the AuthRequired status event before AuthPending is returned.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(channels): resume agent turn after auth + pairing completion

After the web UI submits a token via /api/chat/auth-token or approves
pairing via /api/pairing/{channel}/approve, the agent's turn was stuck
at Pending forever — these HTTP handlers configured the extension
directly but never signaled the agent loop to resume.

Fix: inject a follow-up message through msg_tx (the agent's message
channel) after successful auth/pairing. This uses the same pattern as
the OAuth callback handler — the LLM picks up the injected message,
sees the activation/pairing result, and produces a natural response.
The response goes through the full agent pipeline (hooks, safety,
history persistence, Done event).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(channels): also resume agent turn on auth cancel

When the user dismisses the auth card, the frontend calls
/api/chat/auth-cancel which clears auth mode. But the original agent
turn was still paused at Pending with no Done event. The UI stayed
stuck at "Processing..." forever.

Fix: inject a cancellation message through msg_tx so the LLM can
acknowledge the cancellation and the turn completes naturally.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(channels): pass thread_id in pairing approve for proper routing

The injected follow-up message after pairing approval had no thread_id,
causing the gateway to fail with "missing a routing target." The
response from the LLM was produced but couldn't be delivered.

Fix: add optional thread_id to PairingApproveRequest. The frontend
passes currentThreadId so the agent responds in the same conversation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(e2e): add Playwright tests for channel pairing flow

Covers:
- Auth-token/cancel handlers don't 500
- Pairing approve accepts optional thread_id field
- Backward compatibility: approve without thread_id works
- PairingRequired SSE shows pairing card
- PairingCompleted SSE dismisses pairing card
- Frontend sends currentThreadId in pairing approve request body

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(agent): transition thread to Idle on AuthPending

The AuthPending handler was not calling complete_turn() (to avoid
persisting redundant auth instructions as the response), but this
also skipped the ThreadState::Processing → Idle transition. The
thread stayed stuck in Processing forever, so the follow-up message
injected through msg_tx after auth/pairing was silently rejected.

Fix: explicitly set thread.state = Idle in both AuthPending arms
without calling complete_turn().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(e2e): remove dead verification challenge branch from telegram e2e

The Telegram verification challenge flow was removed — channels now
go straight to activation and use the generic pairing flow. The
conditional verification retry in setup_telegram() was dead code.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review comments

- Gate TELEGRAM_TEST_API_BASE_ENV and telegram_api_base_url() behind
  cfg(any(test, debug_assertions)) to prevent production env var override
  (serrrfirat HIGH — ship blocker)
- Sanitize validate_telegram_token() error messages to avoid leaking bot
  tokens via reqwest Display (Copilot)
- Log failed msg_tx sends instead of silently dropping (ilblackdragon)
- Forward thread_id in PairingCompleted SSE event (Copilot)
- Fix stale doc comment on persist_numeric_owner_id (Copilot)
- Hoist duplicate parse::<i64>() in propagate_approval (ilblackdragon)
- Delete dead _removed_telegram_verification_test (ilblackdragon)
- Fix always-passing E2E thread_id assertion (Copilot)
- Add V24 migration checksum to checksums.lock

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update PairingStore::approve doc for noop mode

The doc said "silently succeeds" but the implementation returns
NotFound when no database is configured.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: sanitize extension names in agent prompts + live owner_actor_id in spawned tasks

Two hardening fixes from PR review deferrals:

1. Extension names from HTTP request bodies were interpolated directly into
   format strings that become IncomingMessage content fed to the agent loop.
   Add sanitize_extension_name() that strips non-alphanumeric chars and apply
   it at the two prompt injection points in chat_auth_token_handler and
   chat_auth_cancel_handler.

2. start_polling() and start_websocket_runtime() captured owner_actor_id as
   an owned Option<String> at spawn time. After pairing approval, WebSocket
   channels kept using the stale pre-approval value. Change to pass
   Arc<RwLock<Option<String>>> so spawned tasks read the current owner on
   each tick/event.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review findings — TurnOutcome refactor, security hardening, WS parity

Structural changes:
- Replace Thread::complete_turn/fail_turn/interrupt with single
  conclude_turn(TurnOutcome) that makes it impossible to forget the
  turn state. Fixes AuthPending arms leaving Turn stuck at Processing.
- Add TurnOutcome::CompletedSilently for auth-card-only turns.

Security:
- Sanitize channel name in pairing_approve_handler (missed injection site)
- Fix bot token leak in validate_telegram_token — log safe fields
  (is_timeout, is_connect, status) instead of reqwest error display
  which includes the URL containing the token
- Consume stale fallback auth gate before replaying message to prevent
  duplicate agentic runs on repeated OAuth callbacks
- Sanitize channel_name in derive_onboarding user-visible strings
- Add #[must_use] to BridgeOutcome enum

WS/REST parity:
- Add thread_id to WsClientMessage::AuthToken and AuthCancel
- WS AuthToken handler now injects follow-up message via msg_tx
  (matching REST chat_auth_token_handler behavior)
- WS AuthCancel handler now clears engine pending auth and injects
  cancellation message (matching REST chat_auth_cancel_handler)

Cleanup:
- Deduplicate build_runtime_config_updates (manager.rs imports from
  approval.rs instead of maintaining its own copy)
- Downgrade info! to debug! for auto-generated secret log
- Downgrade warn! to debug! for OAuth fallback diagnostic
- Upgrade debug! to warn! for on_start failure in propagate_approval
- Rename misleading e2e test to match what it actually tests
- Add mixed-character truncation test for sanitize_extension_name

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* test(e2e): add critical coverage for auth flow security and msg_tx injection

New e2e tests:
- test_auth_cancel_injects_follow_up_message_via_sse: verifies the msg_tx
  injection path actually delivers messages end-to-end (SSE response event
  appears after auth-cancel)
- test_sanitize_extension_name_in_auth_cancel: verifies injection characters
  in extension_name are stripped before reaching the agent loop
- test_pairing_approve_sanitizes_channel_name: verifies channel path param
  is sanitized in pairing approve handler
- test_ws_auth_token_accepts_thread_id: verifies WS auth_token messages
  accept the new thread_id field
- test_ws_auth_cancel_accepts_thread_id: verifies WS auth_cancel messages
  accept thread_id and connection stays alive

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: always inject follow-up message after auth token submission

When result.activated was false, the chat_auth_token_handler skipped
the msg_tx injection. This left the paused turn (Pending with Done
suppressed) permanently stuck — the UI showed "Running tool_install..."
forever.

Now both REST and WS handlers always:
1. Clear auth mode
2. Broadcast AuthCompleted (with success=true/false)
3. Inject a follow-up message via msg_tx

The message content varies based on activation status so the LLM
can respond appropriately.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: revert hot_add to clone-then-shutdown to preserve message_tx receiver

The previous fix (drop write lock before shutdown) removed the channel
from the map before calling shutdown(). This dropped the last strong
Arc reference in the channel manager, killing the forwarding task's
receiver. The router holds its own Arc to the inner WasmChannel, so
propagate_approval's ensure_polling() could still send via message_tx
— but the receiver was dead, causing "channel closed" errors.

Revert to the staging pattern: read-lock to clone the Arc, drop the
lock, shutdown the clone, then write-lock to insert the replacement.
The old entry stays in the map (keeping the forwarding task alive)
until the insert atomically replaces it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: log bot_username set_setting failure instead of silently dropping

Copilot review: the set_setting result for bot_username was silently
dropped with `let _ =`. Now logs at debug level if the DB write fails,
giving visibility into mention detection degradation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: repair message_tx when Channel::start() fails at boot

When a WASM channel is loaded at boot without credentials (fresh DB),
on_start fails (e.g., Telegram deleteWebhook returns 404 with unresolved
{TELEGRAM_BOT_TOKEN}). Previously, message_tx was set BEFORE on_start,
so the sender survived but the receiver (rx) was dropped on error return.
Later, refresh_active_channel restarted polling which cloned the orphaned
sender — every send failed with "channel closed".

Fixes:
- Move message_tx creation AFTER on_start succeeds in Channel::start()
- Add WasmChannel::ensure_message_channel() that creates (tx, rx) if
  message_tx is None or closed, returning the stream for forwarding
- refresh_active_channel calls ensure_message_channel() after on_start
  succeeds and wires up a forwarding task if needed

Also:
- Revert hot_add to match staging exactly (no behavior change needed)
- Remove temporary debug logging (message_tx state before dispatch)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address remaining review comments — stale doc, websockets import

- Update AuthPending doc to reflect TurnOutcome::CompletedSilently
  (was "turn NOT completed", now accurately describes conclude_turn)
- Move `import websockets` inside try block so ImportError is caught
  by the except handler when the package isn't installed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review comments — propagate on_start error, dedupe helpers, tighten tests

- propagate_approval: propagate on_start() error as ActivationFailed
  instead of swallowing it (zmanian review #1)
- router.rs: move test-only HashMap import into mod tests (zmanian #2)
- chat.rs: remove duplicate clear_auth_mode (Copilot review #1)
- e2e: strengthen auth-token assertion to check status 200 + success
  field, remove overlapping test_auth_cancel_returns_success (Copilot #2/#3)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address serrrfirat review — TOCTOU race, missing v2 auth clear, warn log

- ensure_message_channel: single write lock for atomic check-and-create
  (fixes TOCTOU race where concurrent callers could orphan a forwarding task)
- chat_auth_token_handler: add missing clear_engine_pending_auth() call
  (REST/WS parity — WS and REST cancel already had it, REST token did not)
- pairing_approve_handler: debug! → warn! for complete_pairing_approval
  failure (operationally significant — channel won't route until restart)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web,extensions): address review — sanitize agent messages, fix approve propagation, skip double Telegram getMe (#2432)

- Sanitize result.message before interpolation into synthetic agent input
  to prevent prompt injection via crafted validation errors (server.rs + ws.rs)
- Surface complete_pairing_approval() failure to frontend with success=false
  SSE event and ActionResponse::fail instead of silently succeeding
- Return ActionResponse::ok when auth_url is present even if activated=false
  so OAuth flows can progress through the frontend popup
- Skip generic validation_endpoint check for Telegram (validate_telegram_token
  already calls getMe and extracts bot_username — avoids double API round-trip)
- Sanitize generic validation_endpoint error messages to avoid leaking
  sensitive URL paths (e.g. bot tokens) via reqwest::Error Display

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Unify gateway onboarding and pairing flows

* Fix gateway message metadata scoping

* Clean up web gateway warnings

* Fix auth and onboarding regression fallout

* Fix gate resolution and pairing rollback trust boundaries

* Guard legacy agent loop from v2 submissions

* Fix PR review follow-ups for onboarding flow

* Fix CI clippy failure in pairing tests

* Fix onboarding review follow-ups

* Fix clippy warning in skills catalog

* Tighten pairing flow e2e assertions

* Fix onboarding auth review follow-ups

* Fix auth routing and tui clippy lint

* Fix pairing gate handoff in onboarding flow

* Fix clippy guard in mission event scan

* Fix merged clippy regressions

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: serrrfirat <f@nuff.tech>
@github-actions github-actions bot added scope: agent Agent core (agent loop, router, scheduler) scope: channel Channel infrastructure scope: channel/cli TUI / CLI channel scope: channel/web Web gateway channel scope: channel/wasm WASM channel runtime scope: tool/builtin Built-in tools scope: db Database trait / abstraction scope: db/postgres PostgreSQL backend scope: extensions Extension management scope: pairing Pairing mode scope: docs Documentation size: XL 500+ changed lines risk: medium Business logic, config, or moderate-risk modules contributor: core 20+ merged PRs labels Apr 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: medium Business logic, config, or moderate-risk modules scope: agent Agent core (agent loop, router, scheduler) scope: channel/cli TUI / CLI channel scope: channel/wasm WASM channel runtime scope: channel/web Web gateway channel scope: channel Channel infrastructure scope: db/postgres PostgreSQL backend scope: db Database trait / abstraction scope: docs Documentation scope: extensions Extension management scope: pairing Pairing mode scope: tool/builtin Built-in tools size: XL 500+ changed lines staging-promotion

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant