Runtime Terminal Port and Project-ID Hardening

Status: Draft
Author: Agent Orchestrator
Date: 2026-03-28
Target Merge: main

Overview

This design documents two production issues that surfaced in first-run and npm-installed flows:

  1. Direct terminal stuck on CONNECTING... when runtime ports differ from client bundle fallback.
  2. /api/spawn receiving a session ID as projectId, leading to deep-core Unknown project failures.

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

Problem B: Project ID / Session ID Domain Confusion

Root Cause Analysis

A. Build-Time vs Runtime Config Boundary Mismatch

B. Namespace Collision and Inconsistent Validation

Goals

  1. Make direct terminal connection deterministic across runtime-selected ports in prebuilt deployments.
  2. Ensure /api/spawn rejects non-configured project IDs early and predictably.
  3. Normalize dashboard project filter values so invalid query state cannot poison project context.
  4. Preserve backwards compatibility for existing default-port setups.

Non-Goals

  1. Redesign session ID format.
  2. Introduce full typed ID wrappers across all packages in this change.
  3. Remove existing reverse-proxy path-based WS support.

Proposed Design

1. Runtime Terminal Config Endpoint

Add GET /api/runtime/terminal (dynamic, no-store):

2. Runtime-Aware DirectTerminal Connection

Update client connection flow:

  1. Resolve build-time values if available.
  2. Fetch /api/runtime/terminal before socket connect when needed.
  3. Parse and normalize returned values.
  4. Build WS URL from runtime values.
  5. 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:

4. Dashboard Project Filter Normalization

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
404Unknown 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

  1. Keep fixed ports only (14800/14801) and disable auto-shift. Rejected: blocks multi-instance startup and fails on legitimate port conflicts.
  2. Continue relying on NEXT_PUBLIC_* runtime injection. Rejected: production client bundles are build-time materialized.
  3. Accept any projectId in /api/spawn and let core throw. Rejected: late 500 errors and poor API ergonomics.

Risks and Mitigations

  1. Runtime endpoint unavailable. Mitigation: client retains safe fallback and reconnect logic.
  2. Reverse-proxy deployments with custom WS path. Mitigation: preserve proxy-path precedence and include runtime proxy field.
  3. Behavior change for invalid project query. Mitigation: normalization affects only unknown IDs; valid IDs and all remain unchanged.

Validation Plan

Rollout Plan

  1. Merge patch to main.
  2. Release new npm package version containing web/client and API updates.
  3. Announce behavior note:
    • default-port users unaffected
    • runtime-shifted port users no longer hit CONNECTING deadlock

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

  1. Direct terminal connects in npm prebuilt flow even when runtime direct port is not 14801.
  2. /api/spawn never returns 500 for unknown-but-valid-format project IDs.
  3. Invalid project query values do not become active project state.
  4. Existing default-port flows remain functional without configuration changes.