fix: make server.registerOperation() from components reachable via the ops API (#1736) - #1743
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Code Review
This pull request implements a cross-thread bridge for server.registerOperation(), allowing operations registered on worker threads to be reachable by the main-thread ops-API dispatcher. The feedback identifies two key issues: a potential infinite recursion bug in executeRemoteOperation due to parameter shadowing of the Promise resolve/reject functions, and an overly restrictive deletion of body.bypass_auth on the worker thread that could break legitimate internal use cases.
Contributor
|
Reviewed; no blockers found. |
cb1kenobi
reviewed
Jul 9, 2026
kriszyp
pushed a commit
that referenced
this pull request
Jul 9, 2026
- Preserve bypass_auth as forwarded to a worker instead of stripping it: external HTTP requests already have it stripped upstream (handlePostRequest, before any dispatch decision runs), and an internal server.operation(op, context, false) caller must see the same authorize:false behavior whether the op runs locally or via the bridge. ITC is an internal, same-process trust boundary (gemini-code-assist). - Destroy an abandoned streaming result before the explicit 501, since processLocalTransaction already ran the handler and the stream may hold an open fd/cursor/socket (cb1kenobi). - Report a structured-clone failure on forward as 500, not 400 — it's a server-side limitation, not a malformed client request (cb1kenobi). - Fix a worker-crash race where the REMOVE_PORT fast path (splice by threadId, no removePort() call) could beat this thread's own port 'close'/'exit' event, leaving threadExitListeners never fired and an in-flight forward waiting out the full timeout instead of failing fast with 503. Route both removal paths through a shared, dedup-guarded notifyThreadExit (cb1kenobi). - Rename the Promise executor's resolve/reject parameters to promiseResolve/promiseReject for clarity — verified (spec + compiled dist output + runtime test) that the original shorthand-method naming did not actually self-shadow, but the rename removes any doubt for future readers (gemini-code-assist). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cb1kenobi
approved these changes
Jul 10, 2026
cb1kenobi
left a comment
Member
There was a problem hiding this comment.
Human and AI reviewed. No issues, neat feature!
…e ops API (#1736) Components (jsResource resources.js, Plugin API handleApplication) load per-worker, so their registerOperation() calls landed in worker-local OPERATION_FUNCTION_MAP instances that the main-thread ops-API dispatcher never reads — the operation was unreachable for every caller. Add a cross-thread bridge (mirroring the RESOURCE_OPENAPI ITC pattern, but targeted at exactly one worker since execution is side-effecting): - Workers announce each registration (OPERATION_REGISTERED); the main thread records name -> Set<threadId>. - On a dispatch miss, the main thread forwards the request body (OPERATION_EXECUTE_REQUEST) to one live registering worker, rotating across workers and pruning dead ones, and awaits the correlated response with a timeout plus thread-exit rejection. - The worker executes through the normal chooseOperation + processLocalTransaction path, so permission checks run where the operation function and its metadata exist, with the forwarded hdb_user; workers never re-forward, so unknown ops cannot loop. - Streaming results are explicitly rejected (501) rather than failing opaquely; structured-clone failures report clearly in both directions. Unknown operations still fail fast with 400 (the registry gates forwarding, so no timeout is paid for genuinely unknown names). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Preserve bypass_auth as forwarded to a worker instead of stripping it: external HTTP requests already have it stripped upstream (handlePostRequest, before any dispatch decision runs), and an internal server.operation(op, context, false) caller must see the same authorize:false behavior whether the op runs locally or via the bridge. ITC is an internal, same-process trust boundary (gemini-code-assist). - Destroy an abandoned streaming result before the explicit 501, since processLocalTransaction already ran the handler and the stream may hold an open fd/cursor/socket (cb1kenobi). - Report a structured-clone failure on forward as 500, not 400 — it's a server-side limitation, not a malformed client request (cb1kenobi). - Fix a worker-crash race where the REMOVE_PORT fast path (splice by threadId, no removePort() call) could beat this thread's own port 'close'/'exit' event, leaving threadExitListeners never fired and an in-flight forward waiting out the full timeout instead of failing fast with 503. Route both removal paths through a shared, dedup-guarded notifyThreadExit (cb1kenobi). - Rename the Promise executor's resolve/reject parameters to promiseResolve/promiseReject for clarity — verified (spec + compiled dist output + runtime test) that the original shorthand-method naming did not actually self-shadow, but the rename removes any doubt for future readers (gemini-code-assist). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
kriszyp
force-pushed
the
kris/1736-register-operation-cross-thread
branch
from
July 10, 2026 12:14
dc52db3 to
d7a2d0c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1736.
Problem
server.registerOperation()from a component'sresources.js(or Plugin APIhandleApplication) — the standard, documented pattern — was permanently unreachable via the ops API. Components load per worker thread, so the registration lands in that worker's module-localOPERATION_FUNCTION_MAP; the ops-API HTTP dispatcher runs only on the main thread and reads the main thread's own instance of that map. Every caller, includingsuper_user, got400 "Operation '<name>' not found". Only the deprecatedstartOnMainThreadextension hook worked.Fix: a cross-thread bridge (
server/serverHelpers/registeredOperations.ts)Mirrors the existing
RESOURCE_OPENAPIITC request/response pattern, with one deliberate difference: executing an operation is side-effecting, so a request is forwarded to exactly one registering worker — never broadcast-first-wins.registerOperation()still sets its local map, then broadcastsOPERATION_REGISTERED; the main thread recordsname → Set<threadId>.OPERATION_EXECUTE_REQUEST) to one live registering worker — rotating across workers, pruning dead ids — and awaits therequestId-correlated response. Unknown names still fail fast with400(the registry gates forwarding, so no timeout is paid for genuinely unknown operations).hdb_user; the worker executes through the normalchooseOperation(fullverifyPerms, where the operation function and its metadata actually exist) +processLocalTransaction. Workers never re-forward, so an unknown op can't loop.bypass_authis re-stripped on the worker as defense-in-depth, and response correlation verifies the responding thread is the one the request was sent to.503and forgets that worker's registrations (onThreadExithook added tomanageThreads); a wedged worker is bounded by the ops-API network timeout; structured-clone failures report a clear error in both directions instead of an opaque hang.This fixes both component contexts named in the issue investigation (jsResource and
handleApplication) at theregisterOperationlayer, and as a side effect letsserver.operation()on the main thread reach component-registered operations too. Worker-local calls (server.operation()in a worker) are untouched — they hit the local map as before.Explicitly out of scope (follow-up: #1742)
Readablegets an explicit501rather than an opaque failure (needs MessagePort transfer plumbing).isJob: was already ignored byregisterOperation()before this change (never wired tojob_operation_function); unchanged.Testing
integrationTests/components/registered-operation.test.ts(fixture registers ops fromresources.js, 2 worker threads): reachable via ops API with worker-side execution and forwarded user identity; repeated calls; errorstatusCodepropagation (422); stream → explicit 501; unknown op → fast 400. 5/5 pass.authentication,delete,job-queue(97/97),components.test.mjs(25/25).Cross-model review (thorough: Gemini + Codex + domain pass)
No confirmed blockers. Adjudicated notes for reviewers:
503rather than hanging — the residual exposure (op partially executes, then 503) is inherent to any worker-crash window, not new to this path.not found— eventual-consistency window matching component load semantics generally.🤖 Generated with Claude Code
— KrAIs (Claude Opus 4.8), for Kris