Port Registrar Foundation#2332
Open
TalZaccai wants to merge 7 commits into
Open
Conversation
Foundation piece for the port-registrar PR. Standalone in-memory registry keyed by (agentName, role, sessionContextId). API surface: register/release/releaseAllForSession/lookup/hasActiveAllocations. Rejects port 0 and out-of-range; warns (does not throw) on privileged ports or the agentServer's own port. Idempotent on the (agentName, role, sessionContextId) triple. Most-recent-wins lookup semantics match the legacy setLocalHostPort behavior the registrar will subsume in the next commit. Not wired into anything yet — pure class + 19 unit tests, all passing. Integration with appAgentManager / sessionContext / agent-server discovery channel follows in subsequent commits. Co-authored-by: Copilot <[email protected]>
Wire the PortRegistrar (added in 670222d) into the dispatcher's app-agent lifecycle and SDK surface so agents can register OS-assigned ports through a single source of truth. - AppAgentManager now takes a PortRegistrar in its constructor; the legacy per-record 'port' field is replaced by a 'sessionContextId' UUID minted fresh on each initializeSessionContext and cleared in closeSessionContext. setLocalHostPort/getLocalHostPort/getSharedLocalHostPort are now thin shims over the registrar (DEFAULT_ROLE='default'); permission checks and the appAgent-undefined guard in getSharedLocalHostPort are preserved. - closeSessionContext gains a finally backstop that calls releaseAllForSession(sessionContextId) so a forgetful or crashing agent can never leak a registration past its lifetime, even if init itself rejected. - SessionContext SDK gets readonly sessionContextId + registerPort(role,port) returning {release()}. The legacy setLocalHostPort/getSharedLocalHostPort are kept and marked @deprecated so existing agents keep working unchanged. - agent-rpc proxies the new registerPort/releasePort and threads sessionContextId through ContextParams so out-of-process agents see the same view as in-process ones. Registration handles are tracked by regId on the dispatcher side. - DispatcherOptions accepts an optional shared PortRegistrar so a host (agentServer) can wire one instance across all conversations; standalone hosts get a process-private one by default. - Folds in the rubber-duck #3 fix to PortRegistrar.register: re-registering an existing (agent,role,session) triple now deletes+reinserts the entry so Map insertion order reflects recency for lookup tie-breaking, plus a regression test covering the ordering invariant. Mocks updated in dispatcher/test/sessionContext.spec.ts and browser/websiteMemory.mts. Full monorepo build succeeds; 27/27 tests pass (20 portRegistrar + 7 sessionContext). Co-authored-by: Copilot <[email protected]>
Sub-PR C of port-registrar-foundation. Wires PortRegistrar (sub-PR B) into the agentServer host and exposes a read-only lookup API to external clients.
Protocol (agent-server-protocol):
- AGENT_SERVER_DEFAULT_PORT (8999), AGENT_SERVER_DEFAULT_URL constants
- DiscoveryChannelName = 'discovery', DiscoveryInvokeFunctions { lookupPort }
agentServer:
- Constructs a process-wide PortRegistrar and threads it through baseOptions.portRegistrar to every conversation's dispatcher
- Mounts the discovery channel alongside the agent-server channel on each WS connection (multiplexed on the same socket); lookupPort(agent, role) returns {port|null}
- scheduleIdleShutdown() now bails when registrar.hasActiveAllocations() so cached extension clients can reconnect
- Calls registrar.setAgentServerPort(port) once the WS server is listening so 'agent-server' itself is discoverable
webSocketChannelServer:
- New WebSocketChannelServerOptions.originAllowlist (case-insensitive, supports '*' suffix prefix-match; no-Origin always allowed for native clients). Permissive default for v1; agentServer opts in later.
Port literal consolidation (8999 -> AGENT_SERVER_DEFAULT_PORT/_URL):
- agent-server: stop.ts, status.ts
- agent-server-client: agentServerClient.ts default args
- CLI: 12 command files
- vscode-shell, visualStudio webview (dispatcherConnection + main banner), browser extension service worker, uriHandler, commandExecutor, shell args
Dispatcher exports PortRegistrar/PortAllocation/PortRegistrationId so hosts (agentServer today, future shell/web) can inject one.
Build green; 27/27 portRegistrar+sessionContext tests pass.
Co-authored-by: Copilot <[email protected]>
Contributor
There was a problem hiding this comment.
Pull request overview
Introduces the foundational infrastructure for dynamic, discoverable per-agent ports by adding an in-memory PortRegistrar to the dispatcher, exposing a port-registration API via SessionContext (including agent-rpc parity), and hosting a new read-only discovery WS-RPC channel on agentServer. The PR also centralizes the agent-server default port/URL constants to eliminate scattered 8999 literals and adds an Origin allowlist hook to the WebSocket channel server for future hardening.
Changes:
- Add
PortRegistrar(with unit tests) and wire it through dispatcher initialization +AppAgentManagersession lifecycle. - Add port registration surface to
SessionContext(and agent-rpc) and introduce a newdiscoveryWS-RPC channel (lookupPort) on agent-server. - Consolidate default agent-server port/URL constants and replace hard-coded
8999usages across CLI/shell/extensions.
Reviewed changes
Copilot reviewed 39 out of 39 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| ts/packages/vscode-shell/src/agentServerBridge.ts | Use shared default agent-server URL constant instead of hard-coded 8999. |
| ts/packages/utils/webSocketChannelServer/src/server.ts | Add optional originAllowlist support via ws verifyClient during upgrade. |
| ts/packages/uriHandler/src/index.ts | Default CLI port now uses AGENT_SERVER_DEFAULT_PORT. |
| ts/packages/shell/src/main/args.ts | Shell default connect port now uses AGENT_SERVER_DEFAULT_PORT. |
| ts/packages/dispatcher/dispatcher/test/sessionContext.spec.ts | Update tests for new createSessionContext(..., sessionContextId) signature and mock portRegistrar. |
| ts/packages/dispatcher/dispatcher/test/portRegistrar.spec.ts | New unit tests for PortRegistrar behavior (register/lookup/release semantics). |
| ts/packages/dispatcher/dispatcher/src/index.ts | Export PortRegistrar and related types from dispatcher package. |
| ts/packages/dispatcher/dispatcher/src/execute/sessionContext.ts | Add sessionContextId and implement SessionContext.registerPort(). |
| ts/packages/dispatcher/dispatcher/src/context/portRegistrar.ts | New in-memory registrar implementation with session-scoped allocations. |
| ts/packages/dispatcher/dispatcher/src/context/commandHandlerContext.ts | Add registrar to context and dispatcher options; construct/share registrar. |
| ts/packages/dispatcher/dispatcher/src/context/appAgentManager.ts | Generate/manage sessionContextId, route legacy port APIs through registrar, and ensure backstop cleanup on failure/close. |
| ts/packages/commandExecutor/src/commandServer.ts | Default agent-server URL now uses AGENT_SERVER_DEFAULT_URL. |
| ts/packages/cli/src/slashCommands.ts | Use AGENT_SERVER_DEFAULT_PORT for shutdown default. |
| ts/packages/cli/src/commands/server/stop.ts | Use AGENT_SERVER_DEFAULT_PORT as default flag value. |
| ts/packages/cli/src/commands/server/status.ts | Use AGENT_SERVER_DEFAULT_PORT as default flag value. |
| ts/packages/cli/src/commands/run/translate.ts | Use AGENT_SERVER_DEFAULT_PORT as default flag value. |
| ts/packages/cli/src/commands/run/request.ts | Use AGENT_SERVER_DEFAULT_PORT as default flag value. |
| ts/packages/cli/src/commands/run/explain.ts | Use AGENT_SERVER_DEFAULT_PORT as default flag value. |
| ts/packages/cli/src/commands/replay.ts | Use AGENT_SERVER_DEFAULT_PORT as default flag value. |
| ts/packages/cli/src/commands/conversations/rename.ts | Use AGENT_SERVER_DEFAULT_PORT as default flag value. |
| ts/packages/cli/src/commands/conversations/list.ts | Use AGENT_SERVER_DEFAULT_PORT as default flag value. |
| ts/packages/cli/src/commands/conversations/delete.ts | Use AGENT_SERVER_DEFAULT_PORT as default flag value. |
| ts/packages/cli/src/commands/conversations/create.ts | Use AGENT_SERVER_DEFAULT_PORT as default flag value. |
| ts/packages/cli/src/commands/connect.ts | Use AGENT_SERVER_DEFAULT_PORT as default flag value. |
| ts/packages/agentServer/server/src/stop.ts | Use AGENT_SERVER_DEFAULT_PORT when --port absent. |
| ts/packages/agentServer/server/src/status.ts | Use AGENT_SERVER_DEFAULT_PORT when --port absent. |
| ts/packages/agentServer/server/src/server.ts | Create shared registrar for all conversations; add discovery channel; idle shutdown now considers active allocations. |
| ts/packages/agentServer/protocol/src/protocol.ts | Define discovery channel name + RPC type, and add default port/URL constants. |
| ts/packages/agentServer/protocol/src/index.ts | Re-export discovery + default port/URL constants. |
| ts/packages/agentServer/client/src/index.ts | Re-export AGENT_SERVER_DEFAULT_PORT/URL to clients. |
| ts/packages/agentServer/client/src/agentServerClient.ts | Default port parameters now use AGENT_SERVER_DEFAULT_PORT. |
| ts/packages/agentSdk/src/agentInterface.ts | Add sessionContextId + new registerPort() API; deprecate legacy port methods. |
| ts/packages/agents/visualStudio/host/webview/src/main.ts | Display default agent-server URL via shared constant. |
| ts/packages/agents/visualStudio/host/webview/src/dispatcherConnection.ts | Replace hard-coded default URL with shared constant. |
| ts/packages/agents/browser/src/extension/serviceWorker/dispatcherConnection.ts | Replace hard-coded default URL with shared constant. |
| ts/packages/agents/browser/src/agent/websiteMemory.mts | Update SessionContext mock to include registerPort + sessionContextId. |
| ts/packages/agentRpc/src/types.ts | Add registerPort/releasePort RPC methods and require sessionContextId in ContextParams. |
| ts/packages/agentRpc/src/server.ts | Plumb sessionContextId through SessionContext shim; add registerPort implementation for out-of-process agents. |
| ts/packages/agentRpc/src/client.ts | Implement dispatcher-side handlers for registerPort/releasePort and include sessionContextId in context params. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Conflicts in appAgentManager.ts (init/close paths) and vscode-shell agentServerBridge.ts resolved by combining the readiness/setup framework from main with this branch's portRegistrar wiring. Test fixups required by main + this branch: - agentReadiness.spec.ts: AppAgentManager constructor now takes a PortRegistrar — pass new PortRegistrar() - sessionContext.spec.ts (4 call sites): createSessionContext signature gained sessionContextId — pass a literal id Build green, 57/57 portRegistrar+sessionContext+agentReadiness tests pass. Co-authored-by: Copilot <[email protected]>
1. Discovery: special-case 'agent-server' agent name to return the live registered server port via portRegistrar.getAgentServerPort(), so external clients can discover the configured port even when it differs from the bootstrap port. New AGENT_SERVER_DISCOVERY_NAME constant in protocol. 2. Discovery: make 'role' optional on DiscoveryInvokeFunctions.lookupPort and on PortRegistrar.lookup (defaults to DEFAULT_ROLE), matching the documented intent and aligning with what setLocalHostPort registers. 3. agent-rpc client: track regIds per contextId in regIdsByContext and release any unreleased handles in the closeAgentContext wrapper. Prevents handle leaks when an out-of-process agent crashes or forgets to release. releasePort RPC param gains optional contextId so explicit releases also clean up the per-context index. Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
Symmetric client-side counterpart to the discovery channel introduced in commit 3 of this series. Without it, PR 1 ships a server-side WS-RPC handler with no callers and no end-to-end test of the actual wire format -- only an in-process unit test of the registrar. * New `discoverPort(agentName, role?, options?)` exported via a narrow `./discovery` subpath on `@typeagent/agent-server-client`. Returns a tagged result -- `found` / `not-registered` / `unreachable` -- so callers can distinguish "agent isn't loaded yet, retry" from "agentServer isn't running, fall back to a hardcoded default for back-compat" without parsing error strings. * Subpath rather than top-level export so external clients (browser extension, VS Code extension service workers) don't drag in the full main-client surface (fs / os / child_process / dispatcher RPC). The discovery module imports only `agent-rpc`, `isomorphic-ws`, and the small `agent-server-protocol` constants module. * 4 integration tests spin up a real `ws` server speaking the `agent-rpc` channel framing (createChannelProviderAdapter + createRpc) and exercise: `found`, `not-registered`, `unreachable` (server bound and immediately closed), and timeout (server accepts but never resolves). Bootstraps Jest in the package along the way -- one `jest.config.cjs` delegating to the shared root config, plus a `test/` tsconfig matching the convention used by every other test-bearing package in the repo. This unblocks the per-agent migration PRs (2--5): each will import `discoverPort` from the same subpath rather than re-implementing the discovery handshake. Co-authored-by: Copilot <[email protected]>
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.
agentServerhosts many app-agents in one process, and several of those agents open their own WebSocket / HTTP listeners for out-of-process clients (Chrome extension, VS Code extension, the Visual Studio C# plugin, the onboarding helper). Today every one of those listeners hard-codes its port. That has two practical consequences:We want to flip the model: agents bind
port = 0, the OS picks a free port, the agent registers that port with a central registrar, and external clients discover the port over a well-known channel onagentServer.This PR is the foundation only — it builds the registrar, plumbs it through the SDK, stands up the discovery channel, and ships the matching client-side discovery helper. Per-agent migrations follow in PRs 2–5.
What this PR introduces
A shared
PortRegistrarThe registrar is owned by the dispatcher package rather than
agentServeritself, so any future host of the dispatcher (the desktop shell, a hypothetical web API) gets the same model for free.agentServeris just the first host that wires one up.The registrar tracks
(agentName, role) → portallocations, scoped by an opaquesessionContextIdso we can clean up everything an agent registered when its session ends. Re-registering the same(agent, role)replaces the prior entry rather than stacking duplicates — this matches what an agent actually means when it re-binds (e.g. after a config change).A new SDK surface for agents
SessionContextgainsregisterPort(port, role?)/releasePort(id)and exposes a stablesessionContextId. The same surface is mirrored inagent-rpcso out-of-process agents see exactly what in-process agents see.AppAgentManagermints asessionContextIdonce perinitializeSessionContext, clears it oncloseSessionContext, and afinallybackstop callsreleaseAllForSessionso a partially-failed init can't leak allocations.A discovery channel on
agentServerA new WS-RPC channel named
"discovery"is multiplexed onto the same WebSocket as the existingagent-serverchannel. It exposes one read-only call:That's the only operation. Mutation (registering ports) requires being the agent itself, in-process, via
SessionContext.registerPort— there is no remoteregister/releaseon the wire.nullmeans "no such allocation"; callers don't need try/catch around every probe.agentServeralso callssetAgentServerPort()once it's listening, so the host itself is discoverable under the agent nameagent-server— useful for clients that bootstrap from a different known port.A client-side
discoverPort()helperThe symmetric counterpart to the discovery channel, exported via a narrow
./discoverysubpath on@typeagent/agent-server-client:The tagged result is intentional — it lets callers distinguish "agent isn't loaded yet, retry" from "agentServer isn't running, fall back to a hardcoded default for back-compat" without parsing error strings.
The reason it's a separate subpath rather than living on the top-level
agent-server-cliententry: extensions and service workers can't shipfs/os/child_process/ dispatcher RPC. The./discoverysubpath imports onlyagent-rpc,isomorphic-ws, and the smallagent-server-protocolconstants module. PRs 2–5 all reuse this same import.Idle-shutdown safety
agentServer's idle-shutdown timer now bails out whenregistrar.hasActiveAllocations()is true, so cached extension clients can reconnect to a still-running server. The backstop incloseSessionContextensures the registrar drains naturally on session close, so this guard never wedges shutdown indefinitely.Origin-allowlist hook on
webSocketChannelServerwebSocketChannelServeraccepts an optionaloriginAllowlist(case-insensitive, supports a trailing*for prefix-match likechrome-extension://*; native CLI clients with no Origin header are always allowed). The hook is in place so future hardening doesn't require another protocol change.Constants instead of
8999everywhereAGENT_SERVER_DEFAULT_PORT = 8999andAGENT_SERVER_DEFAULT_URLnow live inagent-server-protocol. All the places that previously hard-coded8999(CLI commands, the shell, vscode-shell, the Visual Studio webview, the browser extension service worker,uriHandler,commandExecutor,agentServer/{stop,status}) now import the constant. Behaviorally identical; just removes a class of "I changed it in one place but not the other" bugs.A wire-format change worth calling out
ContextParamsonagent-rpcgains a requiredsessionContextIdfield. This is safe because rpc client and server always build and ship together within this monorepo, but it's worth flagging for anyone watching for compatibility-affecting changes.Validation
pnpm run buildfromts/— green.pnpm run jest-esm --testPathPattern="portRegistrar|sessionContext"— 27/27 passing (registrar + session lifecycle).pnpm --filter @typeagent/agent-server-client test— 4/4 passing fordiscoverPort. These spin up a realwsserver speaking theagent-rpcchannel framing and exercisefound,not-registered,unreachable(server bound and immediately closed), and timeout (server accepts but never resolves) — the missing wire-protocol coverage for the discovery channel.agentServerstarts on the default port, idle-shutdown defers while an allocation is held, and releases the momentcloseSessionContextruns.Reading order for reviewers
The PR is four commits, each independently buildable and reviewable:
feat(dispatcher): add PortRegistrar class with unit tests— pure data structure + tests. Read this first to internalize the model; nothing else depends on the rest of the codebase.refactor(dispatcher): integrate PortRegistrar into AppAgentManager + SDK— wires the registrar into the dispatcher's session lifecycle and exposes the agent-facing API. The interesting bits are thesessionContextIdlifecycle inappAgentManager.tsand the matching surface onagent-rpc.agentServer: add discovery channel + consolidate AGENT_SERVER_PORT— the host wiring. The discovery channel handler inserver.tsis small (one RPC method); most of the diff size is the mechanical8999 → constantreplacement across CLI/shell/extensions.agent-server-client: add discoverPort helper + wire-protocol tests— symmetric client of commit 3. Small standalone helper + 4 integration tests against a realwsserver speaking the actual framing. Bootstraps Jest in the package using the standard repo convention.Follow-up PRs
codeagent (and triviallylocalView) — first consumer ofdiscoverPortbrowseragent service workervisualStudiohost webviewonboarding-scaffolder+ tightenoriginAllowlistonagentServer