Runtime Terminal Port and Project-ID Hardening
+ + + ++ +
Overview
++ This design documents two production issues that surfaced in first-run and npm-installed flows: +
+-
+
- Direct terminal stuck on
CONNECTING...when runtime ports differ from client bundle fallback.
+ /api/spawnreceiving a session ID asprojectId, leading to deep-coreUnknown projectfailures.
+
+ The implementation introduces runtime configuration discovery for terminal WebSocket connection and stricter semantic validation for project identifiers at API and page-data boundaries. +
+ +Problem Statement
+ +Problem A: Terminal WebSocket Port Drift
+-
+
ao startcan auto-select terminal ports at runtime (for example14802/14803) when defaults are occupied.
+ - Direct terminal server listens on
DIRECT_TERMINAL_PORTat runtime.
+ - Browser client previously relied on build-time
NEXT_PUBLIC_DIRECT_TERMINAL_PORT, with fallback14801.
+ - In prebuilt Next.js client bundles,
NEXT_PUBLIC_*values are embedded at build time.
+ - Result: client can attempt
ws://...:14801while server is on14803, leaving UI in permanentCONNECTING.
+
Problem B: Project ID / Session ID Domain Confusion
+-
+
- Session IDs and project IDs share syntax (
[a-zA-Z0-9_-]+).
+ - Orchestrator session IDs are generated as
${project.sessionPrefix}-orchestrator, which can visually resemble project keys.
+ /api/spawnpreviously validated only identifier shape, not membership inconfig.projects.
+ - Invalid but syntactically valid IDs reached core spawn logic and failed late with
500.
+
Root Cause Analysis
+A. Build-Time vs Runtime Config Boundary Mismatch
+-
+
- The server process controls actual runtime port assignment. +
- The browser bundle cannot safely depend on build-time env values for runtime-selected ports. +
- There was no first-party runtime endpoint for client port discovery. +
B. Namespace Collision and Inconsistent Validation
+-
+
- Project IDs and session IDs were treated as plain strings at API boundaries. +
- Some routes sanitize project filters against config; others previously did not. +
- Validation was format-only in
/api/spawninstead of semantic (is configured project).
+
Goals
+-
+
- Make direct terminal connection deterministic across runtime-selected ports in prebuilt deployments. +
- Ensure
/api/spawnrejects non-configured project IDs early and predictably.
+ - Normalize dashboard project filter values so invalid query state cannot poison project context. +
- Preserve backwards compatibility for existing default-port setups. +
Non-Goals
+-
+
- Redesign session ID format. +
- Introduce full typed ID wrappers across all packages in this change. +
- Remove existing reverse-proxy path-based WS support. +
Proposed Design
+ +1. Runtime Terminal Config Endpoint
+Add GET /api/runtime/terminal (dynamic, no-store):
-
+
terminalPortfromTERMINAL_PORT(normalized, fallback14800)
+ directTerminalPortfromDIRECT_TERMINAL_PORT(normalized, fallback14801)
+ proxyWsPathfromTERMINAL_WS_PATH/NEXT_PUBLIC_TERMINAL_WS_PATH(normalized path ornull)
+
2. Runtime-Aware DirectTerminal Connection
+Update client connection flow:
+-
+
- Resolve build-time values if available. +
- Fetch
/api/runtime/terminalbefore socket connect when needed.
+ - Parse and normalize returned values. +
- Build WS URL from runtime values. +
- Reuse the same runtime-aware logic on reconnect attempts. +
Default ports remain unchanged; runtime-shifted ports become deterministic.
+ +3. Semantic Project Validation in /api/spawn
+ Before calling spawn, verify config.projects[projectId] exists. If missing:
-
+
- Return
404withUnknown project: <id>
+ - Record structured observability failure reason +
- Do not invoke core spawn path +
4. Dashboard Project Filter Normalization
+-
+
- keep
"all"
+ - keep only configured project IDs +
- otherwise fallback to first valid configured project (or primary fallback) +
Flow Chart
+ + + + +ao start <project-path>
+ buildDashboardEnv()
+ Picks TERMINAL_PORT / DIRECT_TERMINAL_PORT at runtime
+ start-all launches servers
+ Next.js server + direct-terminal-ws on DIRECT_TERMINAL_PORT
+ resolveConnectionConfig()
+ Build-time NEXT_PUBLIC_* values available and valid?
+ GET /api/runtime/terminal
+ Read runtime env ports, normalize, return config
+ CONNECTED)
+ POST /api/spawn with body.projectId
+ validateIdentifier(projectId)
+ Syntax check — format only
+ config.projects[projectId] exists?
+ sessionManager.spawn()
+ 201 Created + session payload
+ Dashboard/SSE shows active state
+ 404 — Unknown project: <id>
+ Stop early — core spawn not invoked
+ ?project=<value> from dashboard query
+ | Condition | +Result | +
|---|---|
value == "all" |
+ Keep "all" |
+
| value ∈ configured projects | +Keep value | +
otherwise (e.g. session ID like mono-orchestrator) |
+ Fallback to primary valid project | +
+ Invalid values cannot become active project context. +
+Alternatives Considered
+-
+
- Keep fixed ports only (
14800/14801) and disable auto-shift. Rejected: blocks multi-instance startup and fails on legitimate port conflicts.
+ - Continue relying on
NEXT_PUBLIC_*runtime injection. Rejected: production client bundles are build-time materialized.
+ - Accept any
projectIdin/api/spawnand let core throw. Rejected: late 500 errors and poor API ergonomics.
+
Risks and Mitigations
+-
+
- Runtime endpoint unavailable. Mitigation: client retains safe fallback and reconnect logic. +
- Reverse-proxy deployments with custom WS path. Mitigation: preserve proxy-path precedence and include runtime proxy field. +
- Behavior change for invalid project query. Mitigation: normalization affects only unknown IDs; valid IDs and
allremain unchanged.
+
Validation Plan
+-
+
GET /api/runtime/terminalreturns runtime env ports.
+ POST /api/spawnreturns404for unknown project and does not call spawn.
+ - Project filter normalization keeps valid IDs, keeps
all, and falls back on unknown IDs.
+ - Existing
DirectTerminalURL construction tests remain green.
+
Rollout Plan
+-
+
- Merge patch to main. +
- Release new npm package version containing web/client and API updates. +
- Announce behavior note:
+
-
+
- default-port users unaffected +
- runtime-shifted port users no longer hit
CONNECTINGdeadlock
+
+
Release Checklist (Commands)
+# 1) Push branch and open PR
+git push origin fix-runtime-terminal-projectid-hardening
+
+# 2) Ensure patch changeset exists (this PR includes one)
+ls .changeset/five-lamps-heal.md
+
+# 3) CI validation (already required by repo workflows)
+pnpm --filter @composio/ao-web test -- \
+ src/__tests__/api-routes.test.ts \
+ src/lib/__tests__/dashboard-page-data.test.ts \
+ src/components/__tests__/DirectTerminal.test.ts
+
+# 4) After PR merge: version packages from changesets
+pnpm changeset version
+
+# 5) Commit version bumps and changelogs
+git add .
+git commit -m "chore(release): version packages for runtime terminal + spawn hardening"
+
+# 6) Publish
+pnpm release
+
+# 7) Post-release smoke check
+npm i -g @composio/ao@latest
+ao start <project-path>
+# verify terminal works when direct terminal port is non-default
+
+ Acceptance Criteria
+-
+
- Direct terminal connects in npm prebuilt flow even when runtime direct port is not
14801.
+ /api/spawnnever returns500for unknown-but-valid-format project IDs.
+ - Invalid
projectquery values do not become active project state.
+ - Existing default-port flows remain functional without configuration changes. +