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
Terminal Connection Flow
1. User runs
ao start <project-path>
2. CLI runtime setup —
buildDashboardEnv()
Picks TERMINAL_PORT / DIRECT_TERMINAL_PORT at runtime
3.
start-all launches servers
Next.js server + direct-terminal-ws on DIRECT_TERMINAL_PORT
4. Browser opens session page
5.
resolveConnectionConfig()
Build-time NEXT_PUBLIC_* values available and valid?
Yes
Use build-time values directly
No
GET /api/runtime/terminal
Read runtime env ports, normalize, return config
merge
6. Client builds WS URL from resolved runtime config
7. WebSocket connect to direct-terminal-ws
8. Resolve tmux session
Exact match first → hash-prefixed suffix fallback
9. PTY attach succeeds — terminal interactive (
CONNECTED)
Spawn Path
A. User triggers spawn
Dashboard / mobile / API caller
B.
POST /api/spawn with body.projectId
C.
validateIdentifier(projectId)
Syntax check — format only
D. Semantic guard:
config.projects[projectId] exists?
Yes
sessionManager.spawn()
201 Created + session payload
Dashboard/SSE shows active state
No
404 — Unknown project: <id>
Stop early — core spawn not invoked
Project Filter Normalization
Incoming
?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.