Files
ironclaw/scripts
Illia Polosukhin a524bf8aee refactor(gateway): stage 4b slices + empty allowlist — ironclaw#2599 (#2665)
* refactor(gateway): stage 4b slices + empty allowlist — ironclaw#2599

Collapses the last pre-existing back-edges tracked by the boundary
checker and moves the first three small feature slices out of
server.rs.

Stage 4b slices:

- features/logs/       — /api/logs/events, /api/logs/level (GET/PUT)
- features/pairing/    — /api/pairing/{channel} (GET),
                         /api/pairing/{channel}/approve (POST)
- features/status/     — /api/gateway/status (+ GatewayStatusResponse,
                         ModelUsageEntry, each now owned by the slice)

Platform extensions (co-located with existing platform modules so
every caller — handlers/, features/, and the still-shrinking
server.rs — can reach them without a back-edge):

- platform/legacy_auth.rs:
  - handle_legacy_auth_token_submission
  - handle_legacy_auth_cancel
  - clear_auth_mode, clear_auth_mode_for_thread
  Consumers: server.rs chat HTTP shims + platform/ws.rs.
- platform/engine_dispatch.rs:
  - dispatch_engine_submission
  - dispatch_engine_external_callback
  - dispatch_onboarding_ready_followup  (now takes &ExtensionName)
  Consumers: server.rs chat + extensions_setup_submit + features/pairing.
- platform/static_files.rs gains the workspace-backed layout/widget
  readers (read_layout_config, load_resolved_widgets,
  read_widget_manifest, LAYOUT_PATH, WIDGETS_DIR, MAX_WIDGET_* caps).
  handlers/frontend.rs imports them back from platform.

ExtensionName adoption:

- Deletes sanitize_extension_name and its 5 unit tests from
  server.rs. The defensive "never fails, returns 'unknown'" helper is
  replaced with ironclaw_common::ExtensionName validation at the one
  untrusted boundary we still expose (pairing_approve_handler's URL
  path). Invalid names now return 400 at the handler — the old
  behavior sanitized injection-shaped input into a safe-but-nonsense
  string that would never have matched a real extension anyway, so
  this is strictly better telemetry with no loss of reachable
  behavior. Registry-sourced names (derive_onboarding in
  handlers/extensions.rs) drop the sanitize call entirely; the
  comment notes a follow-up to type Extension.name as ExtensionName
  directly.
- Other shared helpers that needed to leave server.rs as collateral:
  images_to_attachments moves to web/util.rs alongside the other
  pure message-building helpers.

ws.rs cleanup:

- Switches GatewayState / PerUserRateLimiter / RateLimiter /
  ActiveConfigSnapshot imports from the server.rs re-export path to
  crate::channels::web::platform::state directly, removing the last
  state-type allowlist entries.

Allowlist:

- scripts/check_gateway_boundaries.py: ALLOWLIST is now empty. All
  eight pre-existing entries (widget helpers, seven ws.rs shim
  symbols) are gone — every relocation landed in platform/. The
  allowlist mechanism stays in place for future narrowly-scoped
  exceptions.

Diff is roughly −700 lines net from server.rs (now ~5,700 down
from ~6,300), spread across three new feature-slice files and two
new platform modules. No behavior change — this is a pure
relocation + one type-boundary upgrade.

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

* refactor(gateway): lock in pairing boundary tests + tighten layout helper visibility — PR #2665

Two valid findings from the PR #2665 review:

- `read_layout_config` was carried over from `handlers/frontend.rs` as
  `pub`, but every caller lives inside `src/channels/web/`. Tightened
  to `pub(crate)` to match the rest of the workspace/widget helpers in
  the same module (Copilot).
- Added regression coverage for the new 400 boundary in
  `features/pairing/` — `parse_channel` now has 8 unit tests pinning
  that it accepts the lowercase / snake_case shapes pairing uses and
  lowercases mixed-case URL paths, and that it rejects empty, path
  traversal, invalid chars, consecutive underscores, edge
  underscores, and oversized input with `StatusCode::BAD_REQUEST`.
  This locks in the stricter contract the PR introduced so a future
  edit can't accidentally regress to silent canonicalization (Copilot).

The third review note (Gemini: `engine_v2` + `engine_v2_enabled`
redundancy in `GatewayStatusResponse`) is a pre-existing wire-contract
shape — `crates/ironclaw_gateway/static/app.js:8120` reads
`engine_v2` and `app.js:8130` reads `engine_v2_enabled`, so dropping
either field without a coordinated frontend change would regress the
browser UI. Out of scope for this PR's pure relocation.

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

* fix(gateway): parse_channel preserves hyphens for slack-relay — PR #2665 review

Copilot's round-2 review caught a real regression I introduced in this
PR: `parse_channel` returned `ExtensionName::new(...)` directly, and
`ExtensionName`'s canonical form folds `-` into `_`. The pairing store
(via `crate::pairing::normalize_channel_name` in `src/pairing/mod.rs`)
only lowercases — it does *not* fold hyphens — so the live WASM channel
`slack-relay` (see `src/channels/wasm/setup.rs` and
`crate::channels::relay::DEFAULT_RELAY_NAME`) stores hyphenated rows
that a folded `slack_relay` query would silently miss. Empty pairing
lists and failed approvals for every `slack-relay` code.

Fix: keep `ExtensionName::new` at the boundary for its rejection
semantics (path traversal, invalid chars, oversize, edge/consecutive
underscores) but discard the typed value. `parse_channel` now returns
the pre-fold lowercased `String`, which flows directly into
`pairing_store.list_pending` / `approve` and
`ext_mgr.complete_pairing_approval`. The two AppEvent / dispatch call
sites that need a typed `ExtensionName` wrap via
`ExtensionName::from_trusted` — same escape hatch staging's
`pairing_approve_handler` was using before this PR moved it. The
module docstring spells out why the discard-and-keep-the-raw-string
dance exists.

Regression test added (`parse_channel_preserves_hyphens_for_slack_relay`)
pinning both `slack-relay` and `SLACK-RELAY` round-trip through
`parse_channel` as `slack-relay`. Existing tests updated for the new
`Result<String, _>` return type. 9 tests pass.

Also fixed a stale comment in `platform/router.rs` (Copilot): the
"feature handlers still inline in server.rs pending migration" note
predated the logs/oauth/pairing/status slices being extracted. Rewrote
to describe the current split. And dropped the forward-looking
`derive_onboarding` comment about typing `Extension.name` as
`ExtensionName` — this PR just demonstrated that such a naive swap
would break `slack-relay` and related hyphenated channels, so the
follow-up is larger than "type the field".

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 22:51:21 +09:00
..