mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
* ci(reborn): run all webui_v2 JS tests in CI; drop deleted e2e ref - Broaden the reborn-tests.yml JS-test glob from static/js/pages/settings to all of crates/ironclaw_webui_v2/static/js (minus node_modules/dist), so the chat/extensions/lib suites run in CI. - Stub DOMPurify.addHook in markdown.test.mjs: renderMarkdown registers a one-time afterSanitizeAttributes hook before sanitizing, so the mock must accept the registration for the sanitize assertion to be exercised. - Drop tests/e2e/scenarios/test_reborn_webui_v2_legacy_channel_connect.py from the reborn-playwright legacy-auth-inputs group (the scenario is removed later in this stack). Mechanical re-slice (1/4) of the fully-reviewed #5604 head178829a4c. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reborn): Slack personal OAuth foundations (dormant, additive) Additive layer for Slack personal OAuth; no user-visible change. The catalog still offers pairing, all pairing endpoints remain intact, and the new OAuth surface stays dormant until the serve/webui wiring lands in the next PR of this stack. - ironclaw_auth: OAuth primitives generalized beyond Google (OAuthCallbackState newtype, OAuthRedirectUri), provider_identity on credential accounts, SLACK_PERSONAL constants, owner-granularity cleanup + provider selector, contract tests. - oauth_provider_client: SlackAuthedUser parsing, expires_in=0 (non-expiring token) fix, exchange logging. - host_api http + host_runtime egress: execute_credential_exchange with fail-closed enforcement + runtime egress contract tests. - oauth_gate: unified OAuth gate driver (Google refactored onto it, OAuthGateProviderRegistry) incl. fallthrough fix; ripple into google_oauth/notion_oauth/oauth_dcr/nearai_mcp. - slack_personal_oauth (new) + product_auth_serve: Slack OAuth start/callback routes, identity hook, failure-HTML signal. - slack_setup: oauth client-id/secret slot + personal_oauth_ready; slack_channel_connection; SlackPersonalUserBinder trait; product_auth_durable cleanup/flows. - slack_user tool: full WASM user-token tool (tools-src + first_party_extensions assets incl. committed wasm). - gsuite account_policy ctor ripple; provider_identity field ripple across struct literals. Transitional #[allow(dead_code)] markers sit on four pub(crate) hooks that the serve/slack_host_beta wiring consumes in the next stack PR; they disappear when those files reach their final reviewed state. Mechanical re-slice (2/4) of the fully-reviewed #5604 head178829a4c. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reborn): swap Slack pairing codes for personal OAuth The accepted swap: Slack relay pairing is retired; users connect via the personal OAuth flow added by the previous PR of this stack. - Remove slack_personal_binding_pairing{,_serve}, slack_pairing_notifier, channel_connection_resume (composition + product_workflow), the /pair slash-command endpoint, and the v1 pairing_approve builtin tool + registry wiring. - host_api: remove the ChannelPairing runtime credential setup variant, adding a tolerant #[serde(other)] Retired variant + wire test so persisted legacy rows still decode. - reborn_cli serve wiring: fill the Slack personal OAuth slot; drop the commands mount and pairing route config. serve_slack.rs loses the legacy env-based setup import path; legacy [slack] fields are ignored here, and the explicit startup rejection lands in the final stack PR. - WebUI v2: OAuth Configure flow, in-chat OAuth card, watchers, shared product-auth-oauth-events lib + tests; pairing JS deleted; i18n x11 locales; asset manifest updated. - e2e scenarios updated; legacy channel-connect scenario removed. - CHANGELOG: dm_policy default pairing->allowlist + Removed entries; registry/channels/slack.json bump; docs/plans updates. Behavior (owner-accepted, CHANGELOG'd): previously-paired v1 Slack users are force-unpaired and reconnect via OAuth; Telegram/WASM self-service pairing via the pairing endpoints is unaffected. Mechanical re-slice (3/4) of the fully-reviewed #5604 head178829a4c. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(reborn-cli)!: reject legacy [slack] config fields at serve startup BREAKING: ironclaw-reborn serve now rejects the legacy [slack] config fields (installation_id, team_id, api_app_id, slack_user_id, user_id, shared_subject_user_id, signing_secret_env, bot_token_env, channel_routes) with an actionable error instead of silently ignoring them. Slack bot credentials and routing are configured from the WebUI channel setup page; per-user identity comes only from Slack OAuth. [slack].enabled / IRONCLAW_REBORN_SLACK_ENABLED still gate whether the channel mounts. Shipped last in the stack and independently revertable: reverting this commit alone restores the silent-ignore behavior without touching the OAuth swap. Includes the reject_legacy_slack_setup_fields regression test and the CHANGELOG Breaking entry. Mechanical re-slice (4/4) of the fully-reviewed #5604 head178829a4c. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * ci(reborn): rename JS-test step to match broadened webui scope Review follow-up on #5643 (CodeRabbit): the step name and empty-result message still said "settings" after the find glob broadened to all of crates/ironclaw_webui_v2/static/js. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): close review findings on Slack OAuth foundations (#5644) Review follow-ups from the multi-agent + Gemini review of the foundations slice: - slack_setup: return the client secret SecretString directly instead of expose->to_string->rewrap (SecretMaterial is a SecretString alias; drops a needless plaintext heap copy). - slack_setup: rollback_failed_activation_save now also deletes the failed save's fresh oauth_client_secret_handle (guarded by the same previous/protected-current reference check as bot/signing handles); extended the rollback test and added an inherited-handle regression. - ironclaw_auth: OAuthProviderIdentitySubject deserializes through validation (#[serde(try_from = "String")] + TryFrom delegating to new) per the types.md newtype rule; the type is new in this stack so no persisted rows predate the tightening. - oauth_provider_client + oauth_dcr: consume the staged network policy after every credential exchange (success or failure) via the obligation handler's abort seam; the egress pipeline only discards on pre-transport errors, so successful exchanges leaked one unbounded policy-store entry per flow. Regression asserts the staged policy is discarded after a successful exchange. - product_auth_serve: the Slack callback identity hook now returns a compensating rollback; if complete_oauth_callback fails after the hook durably bound the Slack identity, the completion-failure arm deletes exactly that binding (scoped by full provider_user_id) so a failed completion cannot leave Slack "connected" with no usable credential. Threaded a RebornUserIdentityBindingDeleteStore through SlackPersonalOAuthBindingConfig; regression drives the route-level harness with a failing completion and asserts bind-then-rollback. - slack_user tool: log only the static Slack API resource name (strip the query string carrying search terms / channel ids / timestamps) in both source copies; committed wasm rebuilt with cargo-component (wasm32-wasip2). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): close review findings on the Slack OAuth swap (#5645) Review follow-ups from the multi-agent + Gemini review of the swap slice: - serve_slack: restore the fail-loud guard for featureless builds. The swap had left the non-slack-v2-host-beta branch returning Ok(None) unconditionally, silently starting without Slack when [slack].enabled=true / IRONCLAW_REBORN_SLACK_ENABLED=true. The enablement helpers are no longer feature-gated and the cfg(not) variant bails with the build-feature message; cfg(not) regression tests reinstated plus a unit test for the shared bool parser. - configure-modal: a blocked about:blank pre-open now surfaces "Authorization popup was blocked." and skips the OAuth mutation instead of burning the server-side flow with no completion watcher (mirrors the in-chat startOnboardingOAuth guard); tests drive the captured authorize handler for both the blocked and unblocked paths. - webui_v2_product_auth: caller-level serve tests for the Slack personal OAuth wiring — bearer-required 401, start through the composed router (asserting the Slack authorize URL + server-side user_scope), fail-closed 503 backend_unavailable when product auth is mounted without the slot (the exact state a dropped webui_serve wiring block would produce), and callback mounted + fail-closed sanitized. Backed by a tests-only filled-slot seam on SlackPersonalSetupServiceSlot mirroring the production fill path. - slack_host_beta: thread the mounts' user_identity_delete_store into SlackPersonalOAuthBindingConfig (completes the callback binding rollback landed in the foundations slice). - runtime_setup: document that the setup-save hook deliberately activates the channel only — the slack_user companion requires a caller-scoped slack_personal account and is owned by the post-OAuth activation path. - .env.example: document IRONCLAW_REBORN_SLACK_PERSONAL_OAUTH_REDIRECT_URI alongside the Reborn Slack serve settings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reborn): pin slack token-parse failures to TokenExchangeFailed Review follow-up from #5604 (Gemini): the two slack parse-failure tests asserted only is_err(), so an unrelated parse failure could keep them green. Both now assert the specific AuthErrorCode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): close manual-E2E findings on the Slack OAuth flows Four findings from live end-to-end testing of the stack: - Restore the pair-once-resume-all behavior OAuth lost in the swap: a completed auth flow's continuation references at most one run (TurnGateResume) or none (SetupOnly from the Settings surface), so authorizing Slack in one chat left the caller's other chats parked on BlockedAuth — the deleted channel_connection_resume machinery was the only cross-thread fan-out and nothing replaced it. A new BlockedAuthResumeFanout decorator wraps the continuation dispatcher: after the primary dispatch it scans the durable turn-state snapshot for the caller's other BlockedAuth runs whose credential requirements name the completed flow's provider and resumes each (best-effort, idempotent per flow+run, strict tenant+owner scoping, primary run skipped). Provider-keyed, so multiple chats blocked on Google resume together too — pairing-era parity, generalized. AuthContinuationEvent now carries the completed flow's provider so dispatchers can fan out without re-reading the flow record. Production-shaped builders keep the single-run dispatcher (explicit None) until their snapshot source is wired. - auth-oauth-card: open authorization in a sized popup instead of a new tab — pre-open about:blank with window features, sever the opener, navigate via the shared openAuthPopup reuse path (gate completion travels over localStorage/BroadcastChannel, never window.opener), and surface a blocked popup instead of silently doing nothing. Matches the onboarding and configure flows. - auth-oauth-card: render provider display names ("Slack") instead of naively capitalized raw ids ("Slack_personal") in the authorize CTA and gate shell, with an underscores-to-title-case fallback. - Spinners: the OAuth surfaces used Tailwind's animate-spin, which app.css never defines and whose static-motion policy would suppress anyway; switch to the sheet's sanctioned v2-spin class (the same one the automations refresh spinner uses). Tests: three fan-out regressions (turn-gate completion resumes only the caller's other provider-blocked runs; SetupOnly completion resumes all; resume failures stay best-effort), five auth-oauth-card component tests (display name, prettified unknown ids, sized popup + severed opener + in-place navigation, blocked-popup surface, non-HTTPS refusal), spinner class pins updated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reborn): let Slack disconnect succeed when no workspace setup exists Live-testing follow-up: POST /extensions/slack/remove returned 500 on a fresh instance because disconnect_channel_for_caller fails closed when the personal connection scope is unresolvable — but a never-configured (or setup-deleted) instance has no installation scope at all, so extension uninstall was impossible before Slack was ever set up. The no-scope arm now still revokes the caller's provider-scoped slack_personal credentials, deletes the caller's own Slack identity bindings without an installation prefix (the delete stays tenant + caller-user bound), skips DM targets (installation-keyed and unreachable without a setup), and succeeds. The scoped path is unchanged; the shared cleanup request moved into a helper so the two arms cannot drift. Updated the pinned test to the new contract: disconnect-without-scope succeeds and cleans the caller's bindings unscoped. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(reborn): design spec for Slack bot/tools remodel Approved design-of-record for the model-B remodel (bot = operator entrypoint, tools = user-installable extension) and the three stacked follow-up PRs: remodel / least-privilege scopes / OAuth durability. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): rename Slack extensions (bot slack->slack_bot, tools slack_user->slack) Mechanical rename only, no behavior change - commit 1 of the model-B remodel. Bot channel extension id -> slack_bot; user-tools extension id -> slack (the visible Slack extension). Renames id constants, manifest ids, asset dirs, include paths, and capability ids (slack_user.*->slack.*); cross-crate test refs updated. Unchanged: slack_user_token/slack_bot_token handles, slack_personal provider, Slack adapter/channel and actor kinds, ironclaw_slack_v2_adapter crate, slack-v2-host-beta feature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reborn): model-B Slack backend — bot is hidden channel, tools are the installable extension Commit 2 of the model-B remodel (backend behavior). The Slack tools extension (slack) becomes the visible, user-installable extension; the bot channel (slack_bot) becomes hidden operator infrastructure. - Visibility flip: is_internal_extension_package_ref now hides the bot (slack_bot), not the tools; the catalog/list surface the tools (slack). - Delete the ~150-line companion coupling (activate_slack_with_companion + 5 helpers + call-site branches). The tools extension is a normal visible extension activated directly via the standard lifecycle, so the connect-but-inactive activation-gap bug dissolves — there is no hidden companion that can silently fail to activate. - The tools extension owns the slack_personal OAuth: the credential requirement and the OAuth-start requester check target slack, not the bot. - Unbound-user greeting: a first-contact DM from a Slack user with no identity binding is greeted with a connect nudge (previously silently dropped) — no binding lookup, no agent turn, canned text only. - Operator flow: the bot is fully hidden from the extension catalog/list; operators configure it via the Slack setup panel. Test-first: rewrote the companion/connection-state/search tests for model B and flipped the unbound-user silence test to assert the nudge. All Slack behavior tests pass; the remaining composition failures are pre-existing sandbox scheduler/model-call timeouts plus one flaky trigger test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn-webui): Slack extension is the tools package, not a channel Commit 3 of the model-B remodel (frontend). No behavioral change — the frontend is already model-B-compatible (verified: extensions + chat JS suites pass, 324 tests). The Slack tools extension connects via the standard credential / auth-OAuth-card path (like Gmail); the channel-connection path was the bot's and is superseded by the commit-2 unbound-user greeting. Renames the now-misleading isSlackChannel flag in configure-modal.js to isSlackToolsExtension: under model B the visible slack extension is the user-tools package, not the bot channel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): rebuild slack tools WASM component after capability rename The slack->slack_bot / slack_user->slack rename (e4776999f) updated the wasm-src dispatch keys (slack.search_messages, slack.send_message, ...) and the host-runtime contract test's capability id, but the compiled slack_user_tool.wasm was git-renamed at 100% similarity — never recompiled. The shipped binary still dispatched on the old slack_user.* ids, so every Slack tool call failed at runtime (OperationFailed) and host_runtime_services_injects_personal_xoxp_token_for_slack_user_search_capability failed at CI. Recompiled the component from the renamed source (cargo component build --release --target wasm32-wasip2) and redeployed the artifact. Both github_wasm_runtime_contract slack tests now pass. No source change accompanies this commit — the regression test already lives in github_wasm_runtime_contract.rs and this binary rebuild is precisely what makes it green, so [skip-regression-check]. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reborn): least-privilege per-capability Slack scopes The Slack tools manifest previously declared the full personal-OAuth scope union (including chat:write) on every capability, so a read-only tool like search_messages advertised write access it never uses. Declare each capability's real scopes instead: - The four read tools (search_messages, list_conversations, get_conversation_history, get_user_info) request only the ten read scopes. - Only send_message keeps chat:write (eleven-scope union). The OAuth *setup* request (SLACK_PERSONAL_OAUTH_SETUP_SCOPES) still asks for the union up front — a single consent that covers every tool the user may call — because per-scope, opt-in-on-first-write consent is a larger UX effort tracked in #5669. This change makes the per-capability manifest truthful now and documents the follow-up at the constant. Tests: - available_extensions: slack_read_only_tools_do_not_request_chat_write asserts only send_message carries chat:write in the manifest. - github_wasm_runtime_contract: slack_user_scopes() drops chat:write so the read-capability injection fixtures match the read-only manifest. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): compile composition tests without slack-v2-host-beta compose_provider_client_with_runtime takes a `#[cfg(feature = "slack-v2-host-beta")]`-gated 5th parameter (slack_personal_oauth_slot). The production wrapper gates the argument it forwards with the same cfg, but the four `#[tokio::test]` call sites in product_auth_providers.rs passed `None` unconditionally. Without the feature the function takes 4 args, so the tests supply one too many and fail to compile (E0061) — breaking `cargo test` and `cargo clippy --tests` in any default-feature build of this crate. Gate each test call site's 5th argument exactly like the production wrapper. The composition lib-tests now compile under both the default and the slack-v2-host-beta feature sets. Why CI stayed green until now: code_style.yml runs the SLIM clippy matrix (--all-features only) for pull_request and merge_group, where the feature is on and the tests compile. Only the FULL matrix on push-to-main adds the no-features `default` leg that trips E0061 — so this latent break would have turned main red after the Slack stack merged. That default leg is the regression guard for this fix. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(reborn): drop stray blank line left by /pair command removal Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): drop dead slack_manifest_toml() after model-B ingress repoint The Slack events host-ingress route now projects from the bot manifest (slack_bot_manifest_toml), so slack_manifest_toml() has no callers and trips clippy's dead_code lint under -D warnings. The SLACK_MANIFEST const it wrapped is still used directly for the tools package. Removed the dead wrapper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reborn): durable Slack conversation binding (survives process restart) The Slack host-beta record builder constructed an in-memory `InMemoryConversationServices`, so conversation->thread bindings were lost on process restart (the removed `tracing::warn!` admitted exactly that). Thread the conversation services in as a parameter instead: the async production path (`runtime_setup::build_resolver`) now constructs a durable, filesystem-backed `RebornFilesystemConversationServices` over the shared host-state filesystem (backend = libSQL / Postgres / local disk, a property of the root filesystem, shared with the idempotency ledger), while the sync/test entrypoint keeps in-memory (no async context there to rehydrate). A `SlackConversationServices` bundle enforces that the `ConversationBindingService` and `ConversationActorPairingService` handles share one backing store. Coverage: durability is contract-tested by `ironclaw_conversations::filesystem_conversation_services_round_trip_persisted_state_on_reopen` (write a binding, reopen a fresh store over the same filesystem, read it back — the filesystem equivalent of the libSQL/Postgres restart-replay tests); the async Slack path building and routing DMs with the durable store in place is covered by the `slack_host_beta` runtime-mount regression tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): display the Slack tools extension as "Slack" Drop the stale "(personal)" qualifier from the tools package display name. It was inherited from the pre-model-B "slack_user" package, when a visible bot "Slack" and visible tools needed to be told apart. Model-B hides the bot, so the qualifier now contrasts with something the user cannot see. Rename the display name (manifest + the mirrored Rust literal), the 5 schema titles, and trim the description reference to the hidden bot channel. Identity is unchanged (id stays "slack"); this is display-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): match Slack tool asset dirs to the extension id Rename the tools schema/prompt asset directories from the legacy slack_user/ (the pre-model-B id) to slack/, matching the extension id and the house convention (github uses schemas/github/). Updates the git paths, the manifest input/output/prompt refs, and the include_bytes! mounts in slack_assets(). The WASM binary filename (slack_user_tool.wasm) and the slack_user_token credential handle keep their legacy names (identity/build artifacts, out of scope). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(reborn): rustfmt-collapse slack_package call after shortening the label Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): report all rejected legacy Slack fields at once Address CodeRabbit review on #5646: the legacy-config rejection in resolve_slack_config_for_serve short-circuited on the first deprecated [slack] field, so an operator fixing one field would rediscover the next only on the next boot. Collect every violated field (including channel_routes) into one bail message, and add a regression test that drives the channel_routes rejection branch through resolve_slack_config_for_serve. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): tidy Slack tools ext per CodeRabbit review on #5668 Three review nits on the model-B Slack tools extension: - get_user_info claimed it returns the user's email, but the granted scope is `users:read` (not `users:read.email`), so email is never available. Drop "email" from the capability description, the model-facing prompt doc, and the params doc comment so the model does not promise a field it cannot fetch. - action_from_context dropped the serde parse error via `map_err(|_| ...)`. Carry the cause (`invalid_invocation_context: {e}`) so a malformed invocation context is diagnosable. Recompiled slack_user_tool.wasm (wasm32-wasip2) with the change. - Extract the bare `"slack"` magic string in configure-modal.js into a named `SLACK_TOOLS_EXTENSION_ID` constant. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): classify Slack conversation-store outage as 503, not 401 Address CodeRabbit review on #5693: when the durable filesystem conversation-binding store fails to initialize, build_resolver mapped the error to SlackIngressError::InstallationNotFound, which ingress_error_response renders as 401 Unauthorized. A storage/infra outage is not an authentication failure — a 401 tells Slack the installation is unauthorized (misleading, and Slack will not meaningfully retry), and the underlying cause was dropped. Add a dedicated SlackIngressError::ConversationStoreUnavailable variant that carries the cause, map the resolver error to it, and render it as 503 Service Unavailable (TemporarilyUnavailable) so Slack retries delivery. Add a regression test pinning the 503 mapping. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): revoke exclusive extension credential on removal Regression from the Slack pairing->OAuth swap (2ce8807ab). Before it, Slack used pairing-based, extension-owned credentials that removal cleaned up automatically, so a removed extension could not be silently re-added. OAuth personal credentials are stored `UserReusable` and are preserved across extension removal by default (the crate guardrail: reusable credentials are untouched unless a provider-scoped cleanup opts in). Extension removal did no credential cleanup at all, so after removal the agent re-installed the bundled extension and re-activated on the surviving token — no OAuth re-consent, a security boundary violation. The revocation lives on the single convergence point both removal entrypoints already call — `RebornLocalExtensionManagementPort::remove` (now `remove(package_ref, scope)`) — so neither door can bypass it: the WebUI facade (`LifecycleProductAction::ExtensionRemove`, the door users actually use) and the `builtin.extension_remove` agent capability both route through it. On success `remove` revokes the removed extension's credential providers that are exclusive to it, via the sanctioned `RebornProductAuthServices::cleanup_credentials_for_lifecycle` path (provider-scoped `Uninstall`). Shared vendor credentials are preserved: removing `gmail` does not revoke the `google` token `google-calendar`/ `drive` still use, determined by re-checking every still-installed extension's declared providers. Cleanup is best-effort (never fails or rolls back the removal) and fails safe (revokes nothing) when it cannot prove a provider is unused, so a shared credential is never deleted out from under another extension. Regression tests (both doors converge on the port): - ui_facade_extension_remove_revokes_exclusive_credential_at_convergence_point: drives the WebUI facade `ExtensionRemove` and asserts the port issues exactly one provider-scoped cleanup for the exclusive github provider. - local_dev_extension_remove_revokes_exclusive_credential_so_reactivation_requires_auth: drives the `builtin.extension_remove` agent capability, then asserts re-activate returns auth_required (previously re-activated silently). - local_dev_extension_remove_preserves_shared_credential_used_by_another_extension: gmail+calendar share google; removing gmail keeps calendar activatable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): origin-independent OAuth flow-status poll for reconnect The extension-setup "reconnect" modal could hang forever after OAuth completed. On reconnect the frontend watcher has no configured-state poll fallback (the extension is already configured), so it depends solely on the callback page's same-origin localStorage/BroadcastChannel signal. When the callback runs on a different origin (local ngrok callback vs 127.0.0.1 opener, or split app/callback domains in prod), that signal never reaches the opener tab and the modal never closes. Add a read-only, authenticated, caller-scoped flow-status endpoint (GET /api/reborn/product-auth/oauth/flow/{flow_id}/status) returning the durable AuthFlowStatus by id, and wire the reconnect watcher to poll it as an origin-independent backstop (fire-and-forget with a pending guard; the same-origin browser signal stays the fast path). The response carries the status enum only -- never tokens, PKCE verifiers, authorization codes, or opaque state. Ownership is enforced by get_flow full-scope equality; a flow that is unknown OR owned by another scope both return 404 so the read cannot be a cross-user existence oracle. The browser echoes back the invocation_id the start response minted (callback_scope.invocation_id) so the caller-scoped handler re-derives the exact scope get_flow matched on, while the trusted tenant/user still come from the authenticated caller. Regression coverage: frontend reconnect-with-no-browser-signal completes via the poll (and a failed poll surfaces a retryable error); backend caller-level tests lock completed->"completed" without secrets, malformed id->400, unknown id->404, and cross-scope->404 (not 403). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): post the renamed 'slack' extension id in personal OAuth start tests The model-B remodel renamed the installable Slack tools extension slack_user -> slack (and the bot channel slack -> slack_bot), but slack_personal_oauth_start_serves_through_composed_router / _fails_closed_without_slot still posted "slack_bot" — the hidden bot channel, which is not OAuth-installable — so the handler correctly rejected it (400) and the composed-router test asserted 200 and failed. Post "slack" so the tests exercise the real installable extension. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(reborn): unify activation button loading into the shared Button Both the in-extension (configure-modal) and in-chat (onboarding-pairing-card) activation flows rendered their own copy-pasted `spinnerGlyph()` — a small, cramped, filled quarter-arc glyph placed ad-hoc before the label. Replace both with a single clean `loading` state on the design-system Button: - Button gains a `loading` prop: a stroke-based ring + rounded-cap arc spinner (v2-spin, reduced-motion-safe), auto-disables the button, sets aria-busy, and keeps the label so width doesn't jump. - configure-modal (OAuth connect, pairing connect, save) and onboarding-pairing-card (configure, pairing submit) buttons now pass `loading=...` instead of hand-rolling the glyph; both `spinnerGlyph()` copies are deleted. - Tests updated to assert the button's `loading` prop (the spinner now lives inside Button) instead of the old `disabled`/`v2-spin` mechanism. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reborn): loading state + popup-close feedback for the in-chat OAuth gate The in-chat auth-gate OAuth card only rendered "Open/Re-open Slack authorization" — no loading state, and no feedback if the user closed the popup before finishing (the extension Configure path already put the spinner on its button). Bring it to parity: - While the authorization popup is open, the primary button itself shows the shared clean loading spinner (Button `loading`) with a "waiting to authorize" label — same button, same color, no separate status row. - Watch the popup for closing; after a short grace window (a successful callback closes the popup itself, and the gate then clears via the completion signal / resumed-run projection), surface a "closed before you finished — re-open to try again" notice. - Extract the spinner into design-system/spinner.js so Button's loading state (used here and by the extension flow) shares one clean animation. Completion detection is unchanged (signal + projection backstop); this is UI feedback only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): register the /conversations mount alias for the Slack durable conversation store The Slack host-state ScopedFilesystem (built via slack_host_state_mount_view) registered aliases for slack-personal-binding, slack-channel-routes, slack-setup, and product_workflow/idempotency — but not /conversations. The durable conversation-binding store (RebornFilesystemConversationServices) persists /conversations/state.json, so every inbound Slack event — including a DM to the bot — failed to open the store and was dropped with a 503 ("no mount alias matches scoped path"). The split/7 durability wiring surfaced the failure correctly, but the store itself could never initialize. Add the /conversations alias (mapped to /tenants/{tenant}/shared/slack-conversations) and lock it with a regression case in the mount-view test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): coverage for OAuth disconnect/reconnect/durable-store + Button loading Lands the test-integration agent's coverage on split/7: - Rust (composition, through-the-caller): - ui_facade_extension_remove_preserves_credential_still_shared_with_another_extension (extension_lifecycle.rs) — the providers_still_in_use fail-safe: removing gmail must NOT revoke the google credential still used by google-calendar. - slack_durable_conversation_store_initializes_through_composed_host_state_mount (factory.rs) — drives local_dev_slack_host_state_filesystem -> RebornFilesystemConversationServices::new, asserting /conversations/state.json opens (caller-level guard for the mount-alias fix). - flow_status_route_descriptor_locks_read_only_bearer_policy (product_auth_serve/mod.rs) — locks the reconnect flow-status route as GET / NoBody / bearer-required / AuthenticatedCaller / per-caller / SameOriginOnly. - JS unit: - button.test.mjs — the shared Button `loading` prop (spinner + disabled + aria-busy; idle has none; both variant paths; disabled alone). - auth-oauth-card.test.mjs — button loading + "authorizing" label while the popup is open; closed-before-finish notice after the popup closes. - Fmt-only: webui_v2_product_auth.rs line unwrapped after the slack_bot->slack rename. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(reborn): run reborn-tests on stacked PRs + add webui-v2 no-undef lint Two CI-scope gaps that let bugs through on the stacked Slack PRs: 1. reborn-tests.yml had `pull_request: branches: [main]`, so the entire Reborn test workflow (composition crate-tests, group-tests, webui-v2 JS tests, integration tier) skipped every PR whose base is a feature branch rather than main. A rename in one slice broke a composition test in another and no per-PR run caught it. Drop the branches filter (matching code_style.yml); the changes/classify job still scopes suites by path. merge_group/push stay main-only. 2. Add a `webui-v2-js-lint` (eslint no-undef) gate over static/js. The node --test component suites stub every collaborator through a vm context, so a module referencing an un-imported symbol still passes its unit test and only crashes at runtime. This gate is the regression guard for exactly that class; it immediately caught a pre-existing latent bug — useChatEvents.js calling isTerminalToolStatus without importing it (ReferenceError in the tool-activity upsert path, on main since47772e4594) — fixed here by adding the import. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reborn): post the renamed 'slack' extension id in personal OAuth start tests Cascadesfee5549bcdown to split/5 (the branch that owns the rename), so #5668's own CI goes green rather than the fix being stranded on split/7. The model-B remodel renamed the installable Slack tools extension slack_user -> slack (and the bot channel slack -> slack_bot), but slack_personal_oauth_start_serves_through_composed_router / _fails_closed_without_slot still posted "slack_bot" — the hidden bot channel, which is not OAuth-installable — so the handler correctly rejected it (400) and the composed-router test asserted 200 and failed. Post "slack" so the tests exercise the real installable extension. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): map extension-lifecycle call failures to OperationFailed, not InputEncode Asked "is my slack connected?" after removing Slack, the agent escalated extension_search -> extension_activate("slack"). Activating a not-installed extension returns ProductWorkflowError::InvalidBindingRequest ("available extension was not found"), which lifecycle_error() mapped to RuntimeDispatchErrorKind::InputEncode -> the model saw the nonsensical "the tool input could not be encoded" / invalid_input, with a "requires_changed_input" retry hint, and relayed it to the user. A failure from a lifecycle CALL (activation requirements, activate/install/ remove) is an operation-level failure, not a tool-input encoding problem. Map InvalidBindingRequest / UnsupportedActionKind (and keep Transient/other) to OperationFailed ("the tool operation failed"). The old code also dropped the reason via `.map_err(|_| ...)`; log it at debug! (server-side) since the fixed model-visible summary cannot carry the raw reason (safe-summary invariant), so the failure stays diagnosable instead of vanishing. Input-parsing failures (parse_input, extension_package_ref) keep InputEncode — those are genuinely malformed tool input. Regression: lifecycle_error_maps_call_failures_to_operation_failed_not_input_encode. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): gate the Slack connect-nudge to 1:1 DMs, never shared channels post_connect_nudge_if_unbound_user_message fired for any UserMessage that rejected with BindingRequired — the doc comment said "first-contact DM" but the code never verified it. An unbound user's app-mention in a SHARED channel also rejects with BindingRequired, so the host connect-nudge ("connect your Slack account…") got posted INTO the shared channel, dropping a message addressed to one user where the whole channel sees it. Gate the nudge on the conversation being a 1:1 DM: Slack DM (im) channel ids start with 'D'; shared channels ('C') and multi-person/group DMs ('G') are excluded, and a missing/blank conversation ref fails closed. Matches the existing slack_reply_target_is_personal_dm 'D'-prefix convention. Regression: rejected_unbound_user_message_in_shared_channel_posts_no_connect_nudge (shared channel -> zero posts); the existing DM test still posts exactly one. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Revert "fix(reborn): map extension-lifecycle call failures to OperationFailed, not InputEncode" This reverts commit61c35a6d77. * fix(reborn): add authGate.authorizing to all locales for i18n key parity The in-chat OAuth card added `authGate.authorizing` ("Waiting for {provider}…") to en.js only. The i18n_consistency test (`all_locales_share_the_en_key_set`) requires every locale to share en's key set, so it drifted — surfaced now that the CI-scope fix runs the webui-v2 crate bucket on stacked PRs. Add a translation to all 10 non-en locales (ar/de/es/fr/hi/ja/ko/pt-BR/uk/zh-CN). Regression guard: crates/ironclaw_webui_v2/tests/i18n_consistency.rs all_locales_share_the_en_key_set (already present; now green). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * style(reborn): rustfmt webui_v2_product_auth after slack_bot->slack shortening The #22 fix shortened `post_extension_oauth_start(&app, "slack_bot", ...)` to `"slack"`, which makes the call fit on one line; the two-line wrap it left behind fails `cargo fmt --check`. Normalize it (split/7 already had the one-line form via the cascade conflict resolution, which is why only #5668/ #5670 were red on Formatting). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ci(reborn): require webui-v2-js-lint in the code-style rollup The webui-v2-js-lint (no-undef) job was added but never wired into the `code-style` aggregate's `needs:` + must-succeed result loop, so a JS no-undef regression would run but not block merge. Add it to both so the gate this PR introduced actually gates. It sits in the has_code-guarded must-succeed loop (the job runs whenever has_code is true, the same guard the rollup already short-circuits on). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): drop slack_actor from triggered_delivery_outcome after OAuth-only merge main's triggered_delivery_outcome.rs set SlackHostBetaConfig.slack_actor: None, but this stack removed the slack_actor field (Option<ExternalActorRef>, a preselected Slack user) as part of the pairing->OAuth swap — OAuth-only uses durable personal bindings, no preselected user. The main-merge brought the test but kept the stack's fieldless struct, so it failed E0560 (breaking Clippy(all-features) + the integration-coverage job). Remove the stale field. Verified: cargo test --no-run --test reborn_integration_triggered_delivery_outcome --all-features now compiles. [skip-regression-check] merge-cleanup: removes a reference to a stack-removed field so a main-added test compiles; no behavior change to test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): log discarded OAuth egress-policy execution-context error discard_oauth_egress_policy's doc says a discard failure "is logged", but the oauth_execution_context arm did `Err(_) => return` silently — inconsistent with the sibling handler.abort branch that warns. Add the warn! so the best-effort cleanup failure is diagnosable (error-handling.md: don't drop the cause). [skip-regression-check] log-only change; no testable behavior change (best-effort cleanup path, outcome unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): log dropped serde errors in extension lifecycle capability output Two silent-failure sites (error-handling.md): the dispatch output serialization mapped its serde error to OutputDecode via map_err(|_| ...) (cause dropped), and channel_connection_display_preview did serde_json::to_string(requirement).ok()? which silently returns None on failure — meaning the in-chat OAuth connection panel would silently never open. Add debug! logs before each so both are diagnosable; behavior is unchanged (still OutputDecode / still skips the preview, just no longer silently). [skip-regression-check] log-only diagnostics; no testable behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reborn): post Slack connect nudge on the workflow-error path An unbound user's first-contact DM resolves as a `BindingRequired` workflow error (ScopeNotFound -> status 404), which the runner routes to `observe_workflow_error`, NOT `observe_workflow_ack`. The connect nudge was wired only into `observe_workflow_ack`, so an unbound 1:1 DM got total silence instead of the "connect your Slack account" prompt -- the nudge had never actually fired in production. Wire `post_connect_nudge_if_unbound_user_message` into `observe_workflow_error` too, mirroring the ack-path ordering: authorized rejection hint first, then the connect nudge for unbound 1:1 DMs. Regression test `unbound_user_message_via_workflow_error_posts_connect_nudge` drives the real error observer path -- the coverage that was missing. The existing test only called `observe_workflow_ack` directly with a synthetic `Rejected` ack, masking the gap (the repo's "test through the caller, not just the helper" rule). Proven red -> green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * Fix Slack OAuth live canary setup * Tighten Slack OAuth live canary checks * Seed Slack OAuth redirect for live QA * Redact Slack setup API failure bodies * Require real Slack personal auth in live QA * Accept Slack admin setup surface in live QA * Harden Slack live QA credential guards * Address Slack OAuth foundation review * Address Slack OAuth swap review * Cover legacy Slack config rejection in serve * Clean up retired Slack user install on restore * Use effect metadata for Slack write-scope test * Address Slack OAuth durability review * Implement AsRef for OAuth identity subject * Align channel install toast test * Restore pairing card i18n test harness * Address Slack live canary review feedback * fix(hooks): bound libsql predicate connections under write lock * Harden live canary Playwright install * test(reborn): align Playwright expectations with Slack setup remodel * test(reborn): update Slack canary setup label * Fix WebUI v2 lint workflow after pnpm migration * Enable pnpm before WebUI lint cache setup * Fix QA 7C canary sheet prompt --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: firat.sertgoz <firat.sertgoz@near.ai> Co-authored-by: serrrfirat <f@nuff.tech>