* docs: clarify one-orchestrator-per-project model (closes #2005) The architecture design doc defined "Orchestrator" only as "the central server that manages everything," conflating the central daemon with the per-project orchestrator agent. This implied a single orchestrator agent spanning all projects. The actual (and intended) model: one central daemon (`ao start`) manages all registered projects, but each project gets its own orchestrator agent session — one per project, enforced by the canonical orchestrator in session-manager (ensureOrchestrator). Disambiguate the term in the nomenclature table and the server-centric design principle. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: remove stale design/architecture artifacts (closes #2005) Delete historical design research, implementation specs, and PR-handoff notes that are superseded by the shipped code and the living docs (DESIGN.md, CLAUDE.md, docs/ARCHITECTURE.md, docs/DEVELOPMENT.md). None were linked from any current doc. Removed: - artifacts/ — original design research (incl. architecture-design.md, the doc #2005 flagged for the misleading "one central orchestrator" framing; removing it resolves the ambiguity at the source) - docs/design/ — Feb 2026 dashboard design-research artifacts - docs/specs/ + design-*.html — implemented feature specs/analyses - docs/state-machine-stage2-evidence-recovery-plan.md — shipped plan - docs/SECURITY-AUDIT-SUMMARY.md — point-in-time audit snapshot - handoff/pr-1466/ — handoff notes for a long-merged PR Drop the now-dangling artifacts/ entry from .cursor/BUGBOT.md ignore list. Confirms the one-orchestrator-per-project model: code enforces a single canonical orchestrator per project (ensureOrchestrator); no multiple orchestrators within one project. * docs: restore docs flagged as still-relevant by review (#2005) @yyovil flagged several docs as still needed. Restore the full artifacts/ and docs/design/ trees, docs/specs/project-based-dashboard- architecture.md, and handoff/pr-1466/ (kept whole for link cohesion). artifacts/architecture-design.md is kept WITH the one-orchestrator-per- project clarification (the #2005 fix) rather than deleted — resolving the ambiguity in place. Restores the artifacts/ entry in .cursor/BUGBOT.md. Remaining removals are limited to clearly point-in-time/superseded docs: docs/state-machine-stage2-evidence-recovery-plan.md, docs/SECURITY-AUDIT-SUMMARY.md, and four design/spec HTML analyses.
This commit is contained in:
parent
c633a7ac49
commit
5d0b624fbe
|
|
@ -32,7 +32,7 @@ Human only intervenes when notified. Everything else is handled.
|
|||
### Design Principles
|
||||
|
||||
1. **Push, not pull**: Notifications are the primary interface. Dashboard is secondary drill-down.
|
||||
2. **Server-centric**: All agents report to a central server. The server coordinates everything.
|
||||
2. **Server-centric**: One central daemon (`ao start`) manages every registered project, and all agents report to it. Each project gets its own orchestrator agent — one orchestrator per project, never a single orchestrator spanning all projects.
|
||||
3. **Plugin everything**: 8 pluggable abstraction slots. Swap any component.
|
||||
4. **Works out of the box**: Default config (tmux + claude-code + worktree + github) requires zero setup beyond `npx agent-orchestrator init`.
|
||||
5. **Silence by default, loud when needed**: Auto-handle routine issues (CI failures, review comments). Only notify the human when their judgment or action is truly required.
|
||||
|
|
@ -44,7 +44,8 @@ Human only intervenes when notified. Everything else is handled.
|
|||
|
||||
| Term | Definition | Examples |
|
||||
| ---------------- | ------------------------------------------ | -------------------------------- |
|
||||
| **Orchestrator** | The central server that manages everything | The Next.js app |
|
||||
| **Orchestrator (daemon)** | The central server process that manages **all** registered projects | `ao start` + the Next.js app |
|
||||
| **Orchestrator agent** | A per-project agent session that spawns and supervises workers — **one per project** | `my-app-orchestrator`, `backend-api-orchestrator` |
|
||||
| **Project** | A configured repository to work on | `my-app`, `backend-api` |
|
||||
| **Session** | A running agent instance working on a task | `my-app-1`, `my-app-2` |
|
||||
| **Runtime** | Where/how the session executes | tmux, docker, k8s, process |
|
||||
|
|
|
|||
|
|
@ -1,362 +0,0 @@
|
|||
# Security Audit Summary — Agent Orchestrator
|
||||
|
||||
**Date**: 2026-02-16
|
||||
**Auditor**: Claude Sonnet 4.5
|
||||
**Scope**: Full codebase + git history secret scanning, automated prevention measures
|
||||
|
||||
## Executive Summary
|
||||
|
||||
✅ **Security audit completed successfully**
|
||||
|
||||
- ⚠️ **1 historical secret found** (OpenClaw token, already removed from current code)
|
||||
- ✅ **0 secrets in current codebase**
|
||||
- ✅ **Automated prevention measures implemented**
|
||||
- ✅ **CI/CD security pipeline added**
|
||||
- ✅ **Documentation updated with security best practices**
|
||||
|
||||
---
|
||||
|
||||
## Findings
|
||||
|
||||
### 1. Historical Secret Leak (RESOLVED)
|
||||
|
||||
**Issue**: OpenClaw notifier token found in git history
|
||||
|
||||
- **Token**: `1af5c4f...872` (redacted - visible in commit history)
|
||||
- **File**: `agent-orchestrator.yaml`
|
||||
- **Commit**: `0393ab70a83e090883895d2168aa39a76f997ec8`
|
||||
- **Date**: 2026-02-15
|
||||
- **Status**: Token already removed from current code, still in git history
|
||||
|
||||
**Impact**: Medium
|
||||
**Likelihood**: Low (local development token, not production)
|
||||
|
||||
**Action Required**:
|
||||
|
||||
- ⚠️ If this token is still in use, **rotate it immediately**
|
||||
- Token is documented in [SECURITY.md](../SECURITY.md)
|
||||
|
||||
### 2. Current Codebase
|
||||
|
||||
**Status**: ✅ **CLEAN**
|
||||
|
||||
Scanned 1.46 MB of code:
|
||||
|
||||
- No hardcoded API keys
|
||||
- No authentication tokens
|
||||
- No passwords or private keys
|
||||
- Test files use dummy values (`test_key`, `https://hooks.slack.com/test`)
|
||||
- Example configs use environment variable references (`${SLACK_WEBHOOK_URL}`)
|
||||
|
||||
---
|
||||
|
||||
## Security Measures Implemented
|
||||
|
||||
### 1. Gitleaks Configuration (`.gitleaks.toml`)
|
||||
|
||||
**Purpose**: Prevent accidental commits of secrets
|
||||
|
||||
**Features**:
|
||||
|
||||
- Uses all default gitleaks rules (covers 100+ secret patterns)
|
||||
- Custom allowlist for false positives
|
||||
- Ignores build artifacts (`node_modules/`, `dist/`, `.next/`)
|
||||
- Allowlists test files (dummy secrets are OK)
|
||||
- Allowlists environment variable references (`${VAR_NAME}`)
|
||||
|
||||
**Patterns Detected**:
|
||||
|
||||
- GitHub tokens (`ghp_*`, `gho_*`, `ghs_*`, `ghu_*`)
|
||||
- Linear API keys (`lin_api_*`)
|
||||
- Slack webhooks & tokens (`xoxb-*`, `xoxa-*`, etc.)
|
||||
- Anthropic API keys (`sk-ant-api03-*`)
|
||||
- OpenAI API keys (`sk-*`)
|
||||
- AWS keys (`AKIA*`)
|
||||
- JWT tokens (`eyJ*`)
|
||||
- Private keys (`-----BEGIN PRIVATE KEY-----`)
|
||||
- Database connection strings (`postgres://user:pass@host`)
|
||||
- Generic API keys (`api_key=...`, `token=...`, `password=...`)
|
||||
|
||||
**Test**:
|
||||
|
||||
```bash
|
||||
# Scan current files
|
||||
gitleaks detect --no-git
|
||||
|
||||
# Scan staged files (pre-commit)
|
||||
gitleaks protect --staged
|
||||
|
||||
# Scan full git history
|
||||
gitleaks detect
|
||||
```
|
||||
|
||||
### 2. Pre-commit Hook (`.husky/pre-commit`)
|
||||
|
||||
**Purpose**: Block commits containing secrets
|
||||
|
||||
**Behavior**:
|
||||
|
||||
- Runs automatically before every `git commit`
|
||||
- Scans only staged files (fast)
|
||||
- Provides helpful error messages if secrets detected
|
||||
- Gracefully skips if gitleaks not installed (with warning)
|
||||
|
||||
**Example Output**:
|
||||
|
||||
```bash
|
||||
🔒 Scanning staged files for secrets...
|
||||
✅ No secrets detected
|
||||
```
|
||||
|
||||
Or if secret detected:
|
||||
|
||||
```bash
|
||||
❌ Secret(s) detected in staged files!
|
||||
|
||||
To fix:
|
||||
1. Remove the secret from the file
|
||||
2. Use environment variables instead: ${SECRET_NAME}
|
||||
3. Add to .env.local (which is in .gitignore)
|
||||
4. Update agent-orchestrator.yaml.example with placeholder values
|
||||
|
||||
If this is a false positive, update .gitleaks.toml allowlist
|
||||
```
|
||||
|
||||
**Setup**:
|
||||
|
||||
- Husky installed as dev dependency
|
||||
- Hook is executable and version-controlled
|
||||
- `prepare` script ensures hook is installed on `pnpm install`
|
||||
|
||||
### 3. GitHub Actions Security Workflow (`.github/workflows/security.yml`)
|
||||
|
||||
**Purpose**: Automated security scanning in CI/CD
|
||||
|
||||
**Jobs**:
|
||||
|
||||
1. **Gitleaks** — Scans full git history on every push/PR
|
||||
2. **Dependency Review** — Scans PRs for vulnerable dependencies
|
||||
3. **NPM Audit** — Detects known vulnerabilities in dependencies
|
||||
|
||||
**Triggers**:
|
||||
|
||||
- Every push to `main`
|
||||
- Every pull request to `main`
|
||||
- Weekly scheduled scan (Monday 8am UTC)
|
||||
|
||||
**Benefits**:
|
||||
|
||||
- Catches secrets missed by pre-commit hook
|
||||
- Prevents secrets from reaching main branch
|
||||
- Alerts on dependency vulnerabilities
|
||||
- Provides security badge for repo
|
||||
|
||||
### 4. Updated `.gitignore`
|
||||
|
||||
**Purpose**: Prevent accidental commits of secret files
|
||||
|
||||
**Added Patterns**:
|
||||
|
||||
```gitignore
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
.env.production.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
|
||||
# Credentials and secrets
|
||||
*.key
|
||||
*.pem
|
||||
*.p12
|
||||
*.pfx
|
||||
*.cer
|
||||
*.crt
|
||||
*.der
|
||||
*.csr
|
||||
secrets.yaml
|
||||
secrets.yml
|
||||
credentials.json
|
||||
credentials.yaml
|
||||
*-credentials.*
|
||||
.secrets/
|
||||
.credentials/
|
||||
|
||||
# API keys and tokens
|
||||
.token
|
||||
.api-key
|
||||
*-token.txt
|
||||
*-api-key.txt
|
||||
|
||||
# Cloud provider credentials
|
||||
.aws/
|
||||
.gcloud/
|
||||
.azure/
|
||||
|
||||
# SSH keys
|
||||
id_rsa
|
||||
id_dsa
|
||||
id_ecdsa
|
||||
id_ed25519
|
||||
*.ppk
|
||||
|
||||
# Local config (may contain secrets)
|
||||
agent-orchestrator.yaml
|
||||
```
|
||||
|
||||
**Critical**: `agent-orchestrator.yaml` is now ignored because it contains user secrets
|
||||
|
||||
### 5. Documentation
|
||||
|
||||
**Created/Updated**:
|
||||
|
||||
1. **[SECURITY.md](../SECURITY.md)** — Security policy & best practices
|
||||
- Responsible disclosure process
|
||||
- Historical audit findings
|
||||
- Developer best practices
|
||||
- User best practices
|
||||
- Required secrets table
|
||||
- Security tools reference
|
||||
|
||||
2. **[README.md](../README.md)** — Added security section
|
||||
- How secret scanning works
|
||||
- Link to SECURITY.md
|
||||
- Environment variable usage examples
|
||||
- Required secrets table
|
||||
|
||||
3. **[docs/DEVELOPMENT.md](./DEVELOPMENT.md)** — Developer security guide
|
||||
- Secret scanning during development
|
||||
- What triggers the scanner
|
||||
- How to handle false positives
|
||||
- Environment variable conventions
|
||||
- Testing locally
|
||||
|
||||
**Key Messages**:
|
||||
|
||||
- ⚠️ **NEVER commit real secrets to git**
|
||||
- ✅ **Always use environment variables**
|
||||
- ✅ **Pre-commit hook will block secrets**
|
||||
- ✅ **CI will catch anything that slips through**
|
||||
|
||||
---
|
||||
|
||||
## Verification
|
||||
|
||||
### Automated Scans
|
||||
|
||||
```bash
|
||||
# ✅ Current codebase scan
|
||||
$ gitleaks detect --no-git
|
||||
INFO: scanned ~1.46 MB in 79.9ms
|
||||
INFO: no leaks found
|
||||
|
||||
# ⚠️ Full git history scan
|
||||
$ gitleaks detect
|
||||
WARN: leaks found: 1
|
||||
Finding: OpenClaw token in commit 0393ab70 (documented)
|
||||
```
|
||||
|
||||
### Security Checklist
|
||||
|
||||
- [x] Gitleaks configuration created and tested
|
||||
- [x] Pre-commit hook installed and working
|
||||
- [x] GitHub Actions security workflow added
|
||||
- [x] `.gitignore` updated with secret patterns
|
||||
- [x] SECURITY.md created with disclosure process
|
||||
- [x] README.md updated with security section
|
||||
- [x] Development docs updated with security practices
|
||||
- [x] All example configs use placeholders (not real secrets)
|
||||
- [x] Test files use dummy values (not real secrets)
|
||||
- [x] Documentation clarifies which env vars are required
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate Actions
|
||||
|
||||
1. **Rotate OpenClaw Token** (if still in use)
|
||||
- Generate new token
|
||||
- Update deployment configs
|
||||
- Revoke old token
|
||||
|
||||
2. **Set Up Required Environment Variables**
|
||||
|
||||
```bash
|
||||
# Add to ~/.zshrc or ~/.bashrc
|
||||
export GITHUB_TOKEN="ghp_..."
|
||||
export LINEAR_API_KEY="lin_api_..."
|
||||
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/..."
|
||||
```
|
||||
|
||||
3. **Verify Pre-commit Hook Works**
|
||||
```bash
|
||||
# Try committing a fake secret (should be blocked)
|
||||
echo "token=ghp_XXXX_fake_token_for_testing_XXXX" > test.txt
|
||||
git add test.txt
|
||||
git commit -m "test" # Should fail with error message
|
||||
rm test.txt
|
||||
```
|
||||
|
||||
### Ongoing Practices
|
||||
|
||||
1. **Code Review**: Check PRs for hardcoded credentials
|
||||
2. **Token Rotation**: Rotate tokens every 90 days
|
||||
3. **Minimal Permissions**: Use read-only tokens when possible
|
||||
4. **Secret Management**: Consider 1Password, AWS Secrets Manager, etc.
|
||||
5. **Monitor CI**: Watch for security workflow failures
|
||||
6. **Update Dependencies**: Keep gitleaks and dependencies up-to-date
|
||||
|
||||
### Before Open-Sourcing
|
||||
|
||||
- [ ] Verify all historical secrets have been rotated
|
||||
- [ ] Confirm no production secrets in git history
|
||||
- [ ] Add security badge to README
|
||||
- [ ] Set up security@composio.dev email alias
|
||||
- [ ] Enable GitHub security features:
|
||||
- [ ] Dependabot alerts
|
||||
- [ ] Code scanning
|
||||
- [ ] Secret scanning (if available for public repos)
|
||||
|
||||
---
|
||||
|
||||
## Tools Used
|
||||
|
||||
| Tool | Purpose | Version |
|
||||
| ----------------------------------------------------- | --------------- | ------- |
|
||||
| [Gitleaks](https://github.com/gitleaks/gitleaks) | Secret scanning | 8.x |
|
||||
| [Husky](https://typicode.github.io/husky/) | Git hooks | 9.1.7 |
|
||||
| [GitHub Actions](https://github.com/features/actions) | CI/CD security | — |
|
||||
|
||||
---
|
||||
|
||||
## Summary Statistics
|
||||
|
||||
- **Files Scanned**: 1.46 MB
|
||||
- **Git Commits Scanned**: 404
|
||||
- **Historical Secrets Found**: 1 (documented, requires rotation)
|
||||
- **Current Secrets Found**: 0
|
||||
- **False Positives**: 0 (test files allowlisted)
|
||||
- **Time to Scan**: ~80ms (current), ~960ms (full history)
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
✅ **Agent Orchestrator is now protected against secret leaks**
|
||||
|
||||
The codebase is currently clean, with one historical secret that needs rotation. Comprehensive automated scanning prevents future accidents. All developers are protected by pre-commit hooks, and CI/CD ensures nothing reaches the main branch.
|
||||
|
||||
**Next Steps**:
|
||||
|
||||
1. Rotate the OpenClaw token if still in use
|
||||
2. Test the pre-commit hook locally
|
||||
3. Monitor CI for security workflow runs
|
||||
4. Review SECURITY.md before first public release
|
||||
|
||||
---
|
||||
|
||||
**Audit completed**: 2026-02-16
|
||||
**Approved for**: Local development, internal testing
|
||||
**Before open-sourcing**: Rotate historical secrets, verify no production credentials
|
||||
|
|
@ -1,743 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Design: CLI Redesign — Simplified Onboarding — Agent Orchestrator</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>
|
||||
<script>mermaid.initialize({ theme: 'dark', startOnLoad: true });</script>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--surface: #161b22;
|
||||
--border: #30363d;
|
||||
--text: #e6edf3;
|
||||
--text-muted: #8b949e;
|
||||
--accent: #58a6ff;
|
||||
--green: #3fb950;
|
||||
--red: #f85149;
|
||||
--yellow: #d29922;
|
||||
--orange: #db6d28;
|
||||
--code-bg: #1c2128;
|
||||
}
|
||||
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
line-height: 1.6;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
|
||||
.meta {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.meta span {
|
||||
margin-right: 24px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.4rem;
|
||||
margin-top: 40px;
|
||||
margin-bottom: 16px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
margin-top: 28px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
ul, ol {
|
||||
margin: 8px 0 16px 24px;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0 24px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
th {
|
||||
background: var(--surface);
|
||||
text-align: left;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid var(--border);
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
tr:nth-child(even) td {
|
||||
background: rgba(22, 27, 34, 0.5);
|
||||
}
|
||||
|
||||
code {
|
||||
background: var(--code-bg);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.85em;
|
||||
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: var(--code-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0 20px;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
pre code {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-new { background: rgba(63, 185, 80, 0.2); color: var(--green); }
|
||||
.badge-changed { background: rgba(210, 153, 34, 0.2); color: var(--yellow); }
|
||||
.badge-removed { background: rgba(248, 81, 73, 0.2); color: var(--red); }
|
||||
.badge-deprecated { background: rgba(219, 109, 40, 0.2); color: var(--orange); }
|
||||
|
||||
.before-after {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin: 16px 0 24px;
|
||||
}
|
||||
|
||||
.before-after > div {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.before-after h4 {
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.before-after .before h4 { color: var(--red); }
|
||||
.before-after .after h4 { color: var(--green); }
|
||||
|
||||
.files-changed {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.files-changed li {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
padding: 2px 0;
|
||||
}
|
||||
|
||||
.files-changed li::before {
|
||||
content: "~";
|
||||
color: var(--yellow);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.files-changed li.new::before {
|
||||
content: "+";
|
||||
color: var(--green);
|
||||
}
|
||||
|
||||
.files-changed li.removed::before {
|
||||
content: "-";
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.callout {
|
||||
background: var(--surface);
|
||||
border-left: 3px solid var(--accent);
|
||||
border-radius: 0 8px 8px 0;
|
||||
padding: 14px 18px;
|
||||
margin: 16px 0 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.callout-decision {
|
||||
border-left-color: var(--yellow);
|
||||
}
|
||||
|
||||
.callout-decision::before {
|
||||
content: "Decision: ";
|
||||
color: var(--yellow);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.scenario-table td:first-child {
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
width: 30px;
|
||||
}
|
||||
|
||||
.mermaid {
|
||||
margin: 16px 0 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 40px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
|
||||
<h1>CLI Redesign: Simplified Onboarding</h1>
|
||||
<div class="meta">
|
||||
<span>PR #463</span>
|
||||
<span>Status: Implemented</span>
|
||||
<span>Updated: 2026-03-17</span>
|
||||
</div>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<h2>1. Overview</h2>
|
||||
|
||||
<p>
|
||||
The CLI redesign eliminates multi-step onboarding by collapsing <code>ao init</code>,
|
||||
<code>ao add-project</code>, and <code>ao start</code> into a single command.
|
||||
The target experience:
|
||||
</p>
|
||||
|
||||
<pre><code>npm install -g @composio/ao && ao start</code></pre>
|
||||
|
||||
<p>
|
||||
Everything else — config generation, project detection, agent runtime discovery,
|
||||
and environment validation — happens automatically. When AO is already running,
|
||||
the CLI detects it and offers contextual options instead of crashing.
|
||||
</p>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<h2>2. Before vs After</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Aspect</th><th>Before</th><th>After</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Setup steps</td>
|
||||
<td>3–4 commands: <code>ao init</code>, edit YAML, <code>ao add-project</code>, <code>ao start</code></td>
|
||||
<td>1 command: <code>ao start</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Config creation</td>
|
||||
<td>Manual via <code>ao init</code></td>
|
||||
<td>Auto-generated on first <code>ao start</code> using environment detection</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Agent selection</td>
|
||||
<td>User edits YAML manually</td>
|
||||
<td>Auto-detected from installed runtimes; interactive picker if multiple</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Spawning a session</td>
|
||||
<td><code>ao spawn <project> <issue></code></td>
|
||||
<td><code>ao spawn <issue></code> (project auto-detected)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Already-running handling</td>
|
||||
<td>Error / undefined behavior</td>
|
||||
<td>Interactive menu (human) or structured info+exit (agent)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Config reference</td>
|
||||
<td>Read source code or docs site</td>
|
||||
<td><code>ao config-help</code> prints annotated schema</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ao init</code></td>
|
||||
<td>Primary setup command</td>
|
||||
<td>Deprecated thin wrapper that calls <code>ao start</code> config path</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>ao add-project</code></td>
|
||||
<td>Required for each repo</td>
|
||||
<td>Fully removed</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="before-after">
|
||||
<div class="before">
|
||||
<h4>Before (4 steps)</h4>
|
||||
<pre><code>ao init
|
||||
# manually edit agent-orchestrator.yaml
|
||||
ao add-project my-app ~/code/my-app
|
||||
ao start</code></pre>
|
||||
</div>
|
||||
<div class="after">
|
||||
<h4>After (1 step)</h4>
|
||||
<pre><code>ao start
|
||||
# config auto-generated, agent auto-detected
|
||||
# dashboard opens at http://localhost:3000</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<h2>3. Architecture Changes</h2>
|
||||
|
||||
<h3>File Map</h3>
|
||||
|
||||
<ul class="files-changed">
|
||||
<li class="new">packages/cli/src/lib/running-state.ts</li>
|
||||
<li class="new">packages/cli/src/lib/caller-context.ts</li>
|
||||
<li class="new">packages/cli/src/lib/detect-agent.ts</li>
|
||||
<li class="new">packages/cli/src/lib/detect-env.ts</li>
|
||||
<li class="new">packages/cli/src/lib/config-instruction.ts</li>
|
||||
<li>packages/cli/src/commands/start.ts</li>
|
||||
<li>packages/cli/src/commands/spawn.ts</li>
|
||||
<li>packages/cli/src/commands/init.ts (rewritten as deprecation wrapper)</li>
|
||||
<li class="removed">packages/cli/src/commands/add-project.ts</li>
|
||||
<li>packages/core/src/orchestrator-prompt.ts</li>
|
||||
<li>packages/core/src/session-manager.ts (env var injection)</li>
|
||||
<li>packages/plugin-agent-claude-code (detect + displayName)</li>
|
||||
<li>packages/plugin-agent-aider (detect + displayName)</li>
|
||||
<li>packages/plugin-agent-codex (detect + displayName)</li>
|
||||
<li>packages/plugin-agent-opencode (detect + displayName)</li>
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
The core architecture (dist-server, session manager, workspace plugins) remains unchanged.
|
||||
Changes are concentrated in the CLI entry layer and the thin coordination between CLI and core.
|
||||
</p>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<h2>4. New Library Files</h2>
|
||||
|
||||
<h3><code>running-state.ts</code> — Single-Instance Tracking</h3>
|
||||
<p>
|
||||
Tracks whether an AO instance is already running. Writes a
|
||||
<code>~/.agent-orchestrator/running.json</code> file containing the running state:
|
||||
</p>
|
||||
|
||||
<pre><code>interface RunningState {
|
||||
pid: number;
|
||||
configPath: string;
|
||||
port: number;
|
||||
startedAt: string;
|
||||
projects: string[];
|
||||
}</code></pre>
|
||||
|
||||
<ul>
|
||||
<li>Uses an advisory lockfile (<code>running.lock</code>) with <code>O_CREAT | O_EXCL</code> for atomic creation — prevents concurrent registration races.</li>
|
||||
<li>Auto-prunes stale entries: if the recorded PID is dead (<code>process.kill(pid, 0)</code> fails), the entry is cleared on read.</li>
|
||||
<li>Lock timeout of 5 seconds with force-remove handles crashed processes that left a stale lock.</li>
|
||||
<li>Exports: <code>register()</code>, <code>unregister()</code>, <code>getRunning()</code>, <code>isAlreadyRunning()</code>, <code>waitForExit(pid, timeoutMs)</code>.</li>
|
||||
</ul>
|
||||
|
||||
<h3><code>caller-context.ts</code> — Human/Orchestrator/Agent Detection</h3>
|
||||
<p>
|
||||
Detects who is invoking the CLI and provides typed helpers for propagating context.
|
||||
</p>
|
||||
|
||||
<pre><code>type CallerType = "human" | "orchestrator" | "agent";</code></pre>
|
||||
|
||||
<ul>
|
||||
<li><code>getCallerType()</code>: If <code>AO_CALLER_TYPE</code> env var is set, trusts it directly. Otherwise: TTY = human, non-TTY = agent.</li>
|
||||
<li><code>isHumanCaller()</code>: Convenience boolean check.</li>
|
||||
<li><code>setCallerContext(env, opts)</code>: Injects <code>AO_CALLER_TYPE</code>, <code>AO_SESSION_ID</code>, <code>AO_PROJECT_ID</code>, <code>AO_CONFIG_PATH</code>, and <code>AO_PORT</code> into a spawn environment record.</li>
|
||||
</ul>
|
||||
|
||||
<h3><code>detect-agent.ts</code> — Plugin-Based Runtime Discovery</h3>
|
||||
<p>
|
||||
No hardcoded binary paths. Dynamically imports each agent plugin and calls its <code>detect()</code> method.
|
||||
</p>
|
||||
|
||||
<p>Known plugins:</p>
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>Package</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>claude-code</td><td><code>@composio/ao-plugin-agent-claude-code</code></td></tr>
|
||||
<tr><td>aider</td><td><code>@composio/ao-plugin-agent-aider</code></td></tr>
|
||||
<tr><td>codex</td><td><code>@composio/ao-plugin-agent-codex</code></td></tr>
|
||||
<tr><td>opencode</td><td><code>@composio/ao-plugin-agent-opencode</code></td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>Selection logic in <code>detectAgentRuntime()</code>:</p>
|
||||
<ul>
|
||||
<li>0 agents detected → default to <code>claude-code</code>.</li>
|
||||
<li>1 agent → auto-select.</li>
|
||||
<li>Multiple + human caller → interactive picker via <code>node:readline/promises</code>.</li>
|
||||
<li>Multiple + non-human → prefer <code>claude-code</code>, else first available.</li>
|
||||
</ul>
|
||||
|
||||
<h3><code>detect-env.ts</code> — Environment Detection</h3>
|
||||
<p>
|
||||
Probes the local environment to auto-populate config fields. Returns an <code>EnvironmentInfo</code> object:
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>Git</strong>: repo detection, remote URL parsing, <code>owner/repo</code> extraction, current branch, default branch (via <code>detectDefaultBranch()</code>).</li>
|
||||
<li><strong>Tools</strong>: tmux availability, gh CLI availability, gh auth status.</li>
|
||||
<li><strong>API keys</strong>: <code>LINEAR_API_KEY</code>, <code>SLACK_WEBHOOK_URL</code> presence.</li>
|
||||
</ul>
|
||||
|
||||
<h3><code>config-instruction.ts</code> — Config Schema Reference</h3>
|
||||
<p>
|
||||
Returns a comprehensive annotated YAML schema covering every config field:
|
||||
ports, defaults (runtime, agent, workspace, notifiers), project settings (repo, path, branch,
|
||||
agentConfig, agentRules, workspace symlinks/postCreate, tracker, SCM),
|
||||
notification channels, and notification routing. Used by <code>ao config-help</code>.
|
||||
</p>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<h2>5. Command Changes</h2>
|
||||
|
||||
<h3><code>ao start</code> <span class="badge badge-changed">changed</span></h3>
|
||||
<p>Now the single entry point for all of AO. On first run:</p>
|
||||
<ol>
|
||||
<li>Calls <code>detectEnvironment(cwd)</code> to probe git, tools, and APIs.</li>
|
||||
<li>Calls <code>detectAgentRuntime()</code> to find an installed agent.</li>
|
||||
<li>Generates <code>agent-orchestrator.yaml</code> with all detected values.</li>
|
||||
<li>Calls <code>register()</code> to write running state to <code>~/.agent-orchestrator/running.json</code>.</li>
|
||||
<li>Starts the dist-server and orchestrator session.</li>
|
||||
</ol>
|
||||
<p>If already running, delegates to the <strong>Already-Running Detection</strong> flow (Section 8).</p>
|
||||
<p>Also exports <code>createConfigOnly()</code> for the deprecated <code>ao init</code> wrapper.</p>
|
||||
|
||||
<h3><code>ao spawn</code> <span class="badge badge-changed">changed</span></h3>
|
||||
<p>
|
||||
Simplified from <code>ao spawn <project> <issue></code> to <code>ao spawn [issue]</code>.
|
||||
The project is always auto-detected — no project argument is accepted.
|
||||
See Section 9 for the auto-detection logic.
|
||||
</p>
|
||||
<p>
|
||||
If a user passes two args (old syntax), a friendly warning is shown:
|
||||
</p>
|
||||
<pre><code>⚠ 'ao spawn <project> <issue>' is no longer supported.
|
||||
The project is now auto-detected. Use:
|
||||
|
||||
ao spawn INT-100 # spawn with issue INT-100
|
||||
ao spawn # spawn without an issue</code></pre>
|
||||
<p>
|
||||
The <code>autoDetectProject()</code> function resolves the project from: single project in config,
|
||||
<code>AO_PROJECT_ID</code> env var, or cwd matching a project path.
|
||||
</p>
|
||||
|
||||
<h3><code>ao batch-spawn</code> <span class="badge badge-changed">changed</span></h3>
|
||||
<p>Same simplification: takes only issue IDs as arguments. The project is always auto-detected — no project prefix is accepted.</p>
|
||||
|
||||
<h3><code>ao init</code> <span class="badge badge-deprecated">deprecated</span></h3>
|
||||
<p>Reduced to a thin wrapper that prints a deprecation warning then delegates to <code>createConfigOnly()</code> from <code>start.ts</code>:</p>
|
||||
<pre><code>// init.ts — full implementation
|
||||
program.command("init")
|
||||
.description("[deprecated] Use 'ao start' instead")
|
||||
.action(async () => {
|
||||
console.log("'ao init' is deprecated. Use 'ao start' instead.");
|
||||
const { createConfigOnly } = await import("./start.js");
|
||||
await createConfigOnly();
|
||||
});</code></pre>
|
||||
|
||||
<h3><code>ao add-project</code> <span class="badge badge-removed">removed</span></h3>
|
||||
<p>Fully deleted. Projects are auto-detected from cwd or added manually to the YAML config file.</p>
|
||||
|
||||
<h3><code>ao config-help</code> <span class="badge badge-new">new</span></h3>
|
||||
<p>Prints the annotated config schema from <code>config-instruction.ts</code> to stdout. Provides both humans and orchestrator agents a quick reference without needing external docs.</p>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<h2>6. Plugin System Enhancements</h2>
|
||||
|
||||
<p>All four agent plugins gained two new exports to support runtime discovery:</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Export</th><th>Type</th><th>Purpose</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>detect()</code></td>
|
||||
<td><code>() => boolean</code></td>
|
||||
<td>Returns <code>true</code> if the agent runtime is available on the system (binary exists in PATH, required API key is set, etc.)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>manifest.displayName</code></td>
|
||||
<td><code>string</code></td>
|
||||
<td>Human-readable name for the interactive picker (e.g. "Claude Code", "Aider", "OpenAI Codex", "OpenCode")</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
These are consumed by <code>detect-agent.ts</code> via dynamic <code>import()</code>.
|
||||
Plugins that fail to import (not installed) are silently skipped — this is by design,
|
||||
as missing plugins are the normal case.
|
||||
</p>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<h2>7. Session Environment Variables</h2>
|
||||
|
||||
<p>
|
||||
Every spawned session (orchestrator and worker) receives these environment variables,
|
||||
set in <code>session-manager.ts</code> (core package) at the single spawn point:
|
||||
</p>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Variable</th><th>Value</th><th>Purpose</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><code>AO_CALLER_TYPE</code></td>
|
||||
<td><code>"orchestrator"</code> or <code>"agent"</code></td>
|
||||
<td>Tells the CLI who is calling — affects interactive prompts and error handling</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>AO_PROJECT_ID</code></td>
|
||||
<td>Project key from config</td>
|
||||
<td>Enables <code>ao spawn <issue></code> auto-detection inside agent sessions</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>AO_CONFIG_PATH</code></td>
|
||||
<td>Absolute path to YAML</td>
|
||||
<td>Sessions find config without filesystem search</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>AO_PORT</code></td>
|
||||
<td>Dashboard port number</td>
|
||||
<td>Agents reach the API without parsing config</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
The <code>setCallerContext()</code> helper in <code>caller-context.ts</code> provides a typed interface
|
||||
for populating these variables. All three spawn paths (orchestrator session, <code>ao spawn</code>, <code>ao batch-spawn</code>)
|
||||
go through the session manager, ensuring consistency.
|
||||
</p>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<h2>8. Already-Running Detection</h2>
|
||||
|
||||
<p>
|
||||
When <code>ao start</code> is invoked and <code>isAlreadyRunning()</code> returns a live state,
|
||||
behavior branches on caller type:
|
||||
</p>
|
||||
|
||||
<div class="mermaid">
|
||||
graph TD
|
||||
A["ao start"] --> B{"isAlreadyRunning()?"}
|
||||
B -->|No| C["Normal startup"]
|
||||
B -->|Yes| D{"getCallerType()"}
|
||||
D -->|human| E["Interactive menu"]
|
||||
D -->|agent / orchestrator| F["Print JSON state + exit 0"]
|
||||
E --> G["Open dashboard in browser"]
|
||||
E --> H["Start new instance — kill old"]
|
||||
E --> I["Override config + restart"]
|
||||
E --> J["Quit"]
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<strong>Human callers</strong> get an interactive menu with four options: open the existing dashboard,
|
||||
start a new instance (killing the old), override with a new config, or quit.
|
||||
</p>
|
||||
<p>
|
||||
<strong>Agent/orchestrator callers</strong> receive a structured info dump (port, PID, projects list)
|
||||
and a clean exit code 0, so they can connect to the already-running instance.
|
||||
</p>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<h2>9. Spawn Auto-Detection Logic</h2>
|
||||
|
||||
<p>The <code>ao spawn</code> argument parser uses this decision tree:</p>
|
||||
|
||||
<div class="mermaid">
|
||||
graph TD
|
||||
A["ao spawn [issue]"] --> B{"Two args provided?"}
|
||||
B -->|Yes| C["⚠ Warning: old syntax<br/>Show correct usage + exit"]
|
||||
B -->|No| D{"One arg provided?"}
|
||||
D -->|No| E["autoDetectProject<br/>no issue — bare session"]
|
||||
D -->|Yes| F["arg = issueId<br/>autoDetectProject"]
|
||||
</div>
|
||||
|
||||
<h3><code>autoDetectProject()</code> Resolution Order</h3>
|
||||
<ol>
|
||||
<li>If only one project in config → use it.</li>
|
||||
<li>If <code>AO_PROJECT_ID</code> env var is set and matches a configured project → use it.</li>
|
||||
<li>If <code>process.cwd()</code> matches a project's <code>path</code> field → use it.</li>
|
||||
<li>Otherwise → throw error listing available projects.</li>
|
||||
</ol>
|
||||
|
||||
<p>This means an orchestrator agent calling <code>ao spawn INT-1234</code> from within a session always resolves correctly via the injected <code>AO_PROJECT_ID</code> env var.</p>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<h2>10. Testing Scenarios</h2>
|
||||
|
||||
<table class="scenario-table">
|
||||
<thead>
|
||||
<tr><th>#</th><th>Scenario</th><th>Expected Outcome</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>1</td><td>Fresh install, <code>ao start</code> in a git repo</td><td>Config auto-generated with detected repo, agent, branch. Server starts.</td></tr>
|
||||
<tr><td>2</td><td>Fresh install, <code>ao start</code> outside git repo</td><td>Error with clear message: "Run ao start inside a git repository."</td></tr>
|
||||
<tr><td>3</td><td><code>ao start</code> when already running (human)</td><td>Interactive menu: open / restart / override / quit.</td></tr>
|
||||
<tr><td>4</td><td><code>ao start</code> when already running (agent)</td><td>JSON state printed to stdout, exit 0.</td></tr>
|
||||
<tr><td>5</td><td><code>ao start</code> with stale PID in running.json</td><td>Stale entry auto-pruned, fresh start proceeds normally.</td></tr>
|
||||
<tr><td>6</td><td><code>ao init</code></td><td>Deprecation warning printed, config created via <code>createConfigOnly()</code>.</td></tr>
|
||||
<tr><td>7</td><td><code>ao spawn #123</code> (single project in config)</td><td>Auto-detects the only project, spawns session for issue #123.</td></tr>
|
||||
<tr><td>8</td><td><code>ao spawn my-app #123</code> (two args)</td><td>Warning: "'ao spawn <project> <issue>' is no longer supported." Shows correct usage and exits.</td></tr>
|
||||
<tr><td>9</td><td><code>ao spawn #123</code> (multi-project, cwd matches one)</td><td>Auto-detects project from cwd path match.</td></tr>
|
||||
<tr><td>10</td><td><code>ao spawn #123</code> (multi-project, no cwd match)</td><td>Error listing available projects.</td></tr>
|
||||
<tr><td>11</td><td><code>ao spawn my-app</code> (arg matches a project ID)</td><td>Treated as issue "my-app" (project always auto-detected). May fail if no project matches cwd/env.</td></tr>
|
||||
<tr><td>12</td><td><code>ao spawn</code> (no args, single project)</td><td>Auto-detect project, no issue — spawns bare session.</td></tr>
|
||||
<tr><td>13</td><td><code>ao batch-spawn #1 #2 #3</code></td><td>Auto-detect project, spawn 3 sessions with duplicate detection.</td></tr>
|
||||
<tr><td>14</td><td><code>ao batch-spawn #1 #2 #3</code> (multi-project, AO_PROJECT_ID set)</td><td>Env var resolves project. Spawn 3 sessions.</td></tr>
|
||||
<tr><td>15</td><td><code>ao config-help</code></td><td>Full annotated YAML schema printed to stdout.</td></tr>
|
||||
<tr><td>16</td><td>Agent session calls <code>ao spawn #123</code> with <code>AO_PROJECT_ID</code> set</td><td>Env var resolves project correctly without cwd match.</td></tr>
|
||||
<tr><td>17</td><td>Multiple agents installed, human runs <code>ao start</code></td><td>Interactive picker with displayName labels shown.</td></tr>
|
||||
<tr><td>18</td><td>Multiple agents installed, non-TTY <code>ao start</code></td><td>Claude Code auto-selected as preferred default.</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<h2>11. Design Decisions</h2>
|
||||
|
||||
<div class="callout callout-decision">
|
||||
<strong>priority field removed from spawn.</strong>
|
||||
Spawning inherits priority from the project config. Per-spawn priority added API surface with no demonstrated use case — callers can override via config if needed.
|
||||
</div>
|
||||
|
||||
<div class="callout callout-decision">
|
||||
<strong>Config instruction NOT injected into orchestrator prompt.</strong>
|
||||
The orchestrator system prompt focuses on session management and coordination. The full config schema (100+ lines of annotated YAML) would bloat the prompt. Instead, the orchestrator can run <code>ao config-help</code> on demand when it needs to modify configuration.
|
||||
</div>
|
||||
|
||||
<div class="callout callout-decision">
|
||||
<strong>dist-server architecture kept unchanged.</strong>
|
||||
The server, session manager, and workspace plugins are out of scope for this PR. The redesign only touches CLI entry points and the thin coordination layer between CLI and core.
|
||||
</div>
|
||||
|
||||
<div class="callout callout-decision">
|
||||
<strong>add-project fully deleted, not deprecated.</strong>
|
||||
Unlike <code>ao init</code> (which gets a deprecation wrapper for existing users), <code>add-project</code> was never on main and had no production users. Clean deletion avoids dead code and import graph bloat.
|
||||
</div>
|
||||
|
||||
<div class="callout callout-decision">
|
||||
<strong>Env vars set in session-manager.ts (core), not spawn.ts (cli).</strong>
|
||||
All three spawn points (orchestrator session, <code>ao spawn</code>, <code>ao batch-spawn</code>) funnel through the session manager. Setting env vars there provides a single source of truth, eliminating the risk of one spawn path forgetting to inject context.
|
||||
</div>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<h2>12. Review Findings & Fixes</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Category</th><th>Finding</th><th>Resolution</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Lockfile spin</td>
|
||||
<td>Busy-wait loop in <code>acquireLock()</code> blocks the event loop for up to 50ms per iteration</td>
|
||||
<td>Acceptable for a single-writer scenario with sub-50ms contention windows. Async alternative adds complexity with no practical benefit.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Stale lock</td>
|
||||
<td>If a process crashes between lock acquire and release, the lockfile persists indefinitely</td>
|
||||
<td>5-second timeout triggers force-remove. Second attempt uses <code>O_EXCL</code> to ensure atomicity.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Race condition</td>
|
||||
<td>Two concurrent <code>ao start</code> invocations could both see "not running"</td>
|
||||
<td><code>O_EXCL</code> on lockfile makes <code>register()</code> atomic — the second writer wins and overwrites. First instance detects the conflict on next state check.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Plugin import errors</td>
|
||||
<td><code>detect-agent.ts</code> silently swallows all import errors, including real bugs</td>
|
||||
<td>By design: missing plugins are the expected case for most users. Import failures (syntax errors in installed plugins) are rare and would surface when that plugin is actually used.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>TTY detection edge</td>
|
||||
<td><code>process.stdout.isTTY</code> is <code>undefined</code> (not <code>false</code>) when piped</td>
|
||||
<td>The ternary <code>isTTY ? "human" : "agent"</code> correctly treats <code>undefined</code> as falsy.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>ID collision</td>
|
||||
<td>If a project ID and an issue ID are identical (e.g. project named "123"), <code>ao spawn 123</code> treats it as an issue</td>
|
||||
<td>All single args are now treated as issue IDs. Project is always auto-detected. No ambiguity possible since project ID is never accepted as a positional arg.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Schema drift</td>
|
||||
<td><code>config-instruction.ts</code> returns a static string that can drift from the actual config type definitions</td>
|
||||
<td>Accepted tradeoff: hand-written annotated comments are significantly more useful than auto-generated JSON Schema output. Drift risk is low given the schema changes infrequently.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Env duplication</td>
|
||||
<td>Env vars were initially set in three CLI commands independently, risking divergence</td>
|
||||
<td>Moved to <code>session-manager.ts</code> in core. All spawn paths use a single <code>setCallerContext()</code> call site.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -1,573 +0,0 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Runtime Terminal Port and Project-ID Hardening</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b1020;
|
||||
--panel: #111a33;
|
||||
--text: #e5ecff;
|
||||
--muted: #9fb0df;
|
||||
--accent: #71a2ff;
|
||||
--border: #2a3d74;
|
||||
--code: #0e1530;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: radial-gradient(1200px 700px at 10% -20%, #223562 0%, var(--bg) 50%);
|
||||
color: var(--text);
|
||||
line-height: 1.55;
|
||||
}
|
||||
main {
|
||||
max-width: 980px;
|
||||
margin: 32px auto;
|
||||
padding: 24px;
|
||||
background: color-mix(in srgb, var(--panel) 92%, black 8%);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
h1, h2, h3 { margin-top: 1.25em; line-height: 1.25; }
|
||||
h1 { margin-top: 0; font-size: 1.9rem; }
|
||||
h2 {
|
||||
font-size: 1.3rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.35rem;
|
||||
}
|
||||
h3 { font-size: 1.05rem; color: #c8d8ff; }
|
||||
p, li { color: var(--text); }
|
||||
.meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(180px, 1fr));
|
||||
gap: 10px 20px;
|
||||
margin: 12px 0 22px;
|
||||
color: var(--muted);
|
||||
}
|
||||
.meta strong { color: var(--text); }
|
||||
ul, ol { padding-left: 1.2rem; }
|
||||
code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: 0.95em;
|
||||
background: var(--code);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: 0.06rem 0.35rem;
|
||||
color: #c6f0ff;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin: 1.4rem 0;
|
||||
}
|
||||
.muted { color: var(--muted); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Runtime Terminal Port and Project-ID Hardening</h1>
|
||||
|
||||
<div class="meta">
|
||||
<div><strong>Status:</strong> Draft</div>
|
||||
<div><strong>Author:</strong> Agent Orchestrator</div>
|
||||
<div><strong>Date:</strong> 2026-03-28</div>
|
||||
<div><strong>Target Merge:</strong> <code>main</code></div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<h2>Overview</h2>
|
||||
<p>
|
||||
This design documents two production issues that surfaced in first-run and npm-installed flows:
|
||||
</p>
|
||||
<ol>
|
||||
<li>Direct terminal stuck on <code>CONNECTING...</code> when runtime ports differ from client bundle fallback.</li>
|
||||
<li><code>/api/spawn</code> receiving a session ID as <code>projectId</code>, leading to deep-core <code>Unknown project</code> failures.</li>
|
||||
</ol>
|
||||
<p>
|
||||
The implementation introduces runtime configuration discovery for terminal WebSocket connection and stricter semantic validation for project identifiers at API and page-data boundaries.
|
||||
</p>
|
||||
|
||||
<h2>Problem Statement</h2>
|
||||
|
||||
<h3>Problem A: Terminal WebSocket Port Drift</h3>
|
||||
<ul>
|
||||
<li><code>ao start</code> can auto-select terminal ports at runtime (for example <code>14802/14803</code>) when defaults are occupied.</li>
|
||||
<li>Direct terminal server listens on <code>DIRECT_TERMINAL_PORT</code> at runtime.</li>
|
||||
<li>Browser client previously relied on build-time <code>NEXT_PUBLIC_DIRECT_TERMINAL_PORT</code>, with fallback <code>14801</code>.</li>
|
||||
<li>In prebuilt Next.js client bundles, <code>NEXT_PUBLIC_*</code> values are embedded at build time.</li>
|
||||
<li>Result: client can attempt <code>ws://...:14801</code> while server is on <code>14803</code>, leaving UI in permanent <code>CONNECTING</code>.</li>
|
||||
</ul>
|
||||
|
||||
<h3>Problem B: Project ID / Session ID Domain Confusion</h3>
|
||||
<ul>
|
||||
<li>Session IDs and project IDs share syntax (<code>[a-zA-Z0-9_-]+</code>).</li>
|
||||
<li>Orchestrator session IDs are generated as <code>${project.sessionPrefix}-orchestrator</code>, which can visually resemble project keys.</li>
|
||||
<li><code>/api/spawn</code> previously validated only identifier shape, not membership in <code>config.projects</code>.</li>
|
||||
<li>Invalid but syntactically valid IDs reached core spawn logic and failed late with <code>500</code>.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Root Cause Analysis</h2>
|
||||
<h3>A. Build-Time vs Runtime Config Boundary Mismatch</h3>
|
||||
<ul>
|
||||
<li>The server process controls actual runtime port assignment.</li>
|
||||
<li>The browser bundle cannot safely depend on build-time env values for runtime-selected ports.</li>
|
||||
<li>There was no first-party runtime endpoint for client port discovery.</li>
|
||||
</ul>
|
||||
|
||||
<h3>B. Namespace Collision and Inconsistent Validation</h3>
|
||||
<ul>
|
||||
<li>Project IDs and session IDs were treated as plain strings at API boundaries.</li>
|
||||
<li>Some routes sanitize project filters against config; others previously did not.</li>
|
||||
<li>Validation was format-only in <code>/api/spawn</code> instead of semantic (<code>is configured project</code>).</li>
|
||||
</ul>
|
||||
|
||||
<h2>Goals</h2>
|
||||
<ol>
|
||||
<li>Make direct terminal connection deterministic across runtime-selected ports in prebuilt deployments.</li>
|
||||
<li>Ensure <code>/api/spawn</code> rejects non-configured project IDs early and predictably.</li>
|
||||
<li>Normalize dashboard project filter values so invalid query state cannot poison project context.</li>
|
||||
<li>Preserve backwards compatibility for existing default-port setups.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Non-Goals</h2>
|
||||
<ol>
|
||||
<li>Redesign session ID format.</li>
|
||||
<li>Introduce full typed ID wrappers across all packages in this change.</li>
|
||||
<li>Remove existing reverse-proxy path-based WS support.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Proposed Design</h2>
|
||||
|
||||
<h3>1. Runtime Terminal Config Endpoint</h3>
|
||||
<p>Add <code>GET /api/runtime/terminal</code> (dynamic, no-store):</p>
|
||||
<ul>
|
||||
<li><code>terminalPort</code> from <code>TERMINAL_PORT</code> (normalized, fallback <code>14800</code>)</li>
|
||||
<li><code>directTerminalPort</code> from <code>DIRECT_TERMINAL_PORT</code> (normalized, fallback <code>14801</code>)</li>
|
||||
<li><code>proxyWsPath</code> from <code>TERMINAL_WS_PATH</code>/<code>NEXT_PUBLIC_TERMINAL_WS_PATH</code> (normalized path or <code>null</code>)</li>
|
||||
</ul>
|
||||
|
||||
<h3>2. Runtime-Aware DirectTerminal Connection</h3>
|
||||
<p>Update client connection flow:</p>
|
||||
<ol>
|
||||
<li>Resolve build-time values if available.</li>
|
||||
<li>Fetch <code>/api/runtime/terminal</code> before socket connect when needed.</li>
|
||||
<li>Parse and normalize returned values.</li>
|
||||
<li>Build WS URL from runtime values.</li>
|
||||
<li>Reuse the same runtime-aware logic on reconnect attempts.</li>
|
||||
</ol>
|
||||
<p class="muted">Default ports remain unchanged; runtime-shifted ports become deterministic.</p>
|
||||
|
||||
<h3>3. Semantic Project Validation in <code>/api/spawn</code></h3>
|
||||
<p>Before calling spawn, verify <code>config.projects[projectId]</code> exists. If missing:</p>
|
||||
<ul>
|
||||
<li>Return <code>404</code> with <code>Unknown project: <id></code></li>
|
||||
<li>Record structured observability failure reason</li>
|
||||
<li>Do not invoke core spawn path</li>
|
||||
</ul>
|
||||
|
||||
<h3>4. Dashboard Project Filter Normalization</h3>
|
||||
<ul>
|
||||
<li>keep <code>"all"</code></li>
|
||||
<li>keep only configured project IDs</li>
|
||||
<li>otherwise fallback to first valid configured project (or primary fallback)</li>
|
||||
</ul>
|
||||
|
||||
<h2>Flow Chart</h2>
|
||||
|
||||
<style>
|
||||
.flow-section {
|
||||
margin: 1.5rem 0 2.5rem;
|
||||
padding: 1.5rem;
|
||||
background: rgba(0,0,0,0.2);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
}
|
||||
.flow-section-title {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--accent);
|
||||
margin: 0 0 1.2rem;
|
||||
padding-bottom: 0.5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.flow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
}
|
||||
.flow-node {
|
||||
position: relative;
|
||||
padding: 12px 20px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
max-width: 520px;
|
||||
width: 100%;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
.flow-node strong { color: #fff; }
|
||||
.flow-node code {
|
||||
font-size: 0.82rem;
|
||||
padding: 0.04rem 0.3rem;
|
||||
}
|
||||
.node-action {
|
||||
background: linear-gradient(135deg, #1a2a55, #1e3468);
|
||||
border: 1px solid #3456a0;
|
||||
color: #cdd9f5;
|
||||
}
|
||||
.node-decision {
|
||||
background: linear-gradient(135deg, #2a1a44, #3a2060);
|
||||
border: 1px solid #6a4ca0;
|
||||
color: #ddd0f5;
|
||||
border-radius: 10px;
|
||||
padding: 14px 24px;
|
||||
}
|
||||
.node-success {
|
||||
background: linear-gradient(135deg, #0d2a1a, #143a24);
|
||||
border: 1px solid #2a8050;
|
||||
color: #b0e8c8;
|
||||
}
|
||||
.node-error {
|
||||
background: linear-gradient(135deg, #2a0d0d, #3a1414);
|
||||
border: 1px solid #803030;
|
||||
color: #e8b0b0;
|
||||
}
|
||||
.node-endpoint {
|
||||
background: linear-gradient(135deg, #1a2040, #1e2855);
|
||||
border: 1px solid #4070b0;
|
||||
color: #a8c8f0;
|
||||
}
|
||||
.flow-arrow {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
color: var(--muted);
|
||||
font-size: 0.75rem;
|
||||
padding: 2px 0;
|
||||
line-height: 1;
|
||||
}
|
||||
.flow-arrow .arrow-line {
|
||||
width: 2px;
|
||||
height: 14px;
|
||||
background: var(--border);
|
||||
}
|
||||
.flow-arrow .arrow-head {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 6px solid var(--border);
|
||||
}
|
||||
.flow-arrow .arrow-label {
|
||||
margin: 2px 0;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
}
|
||||
.flow-branch {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
max-width: 560px;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
}
|
||||
.flow-branch-arm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
gap: 0;
|
||||
}
|
||||
.branch-label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.branch-yes { background: #1a3a24; color: #70d898; border: 1px solid #2a6040; }
|
||||
.branch-no { background: #3a1a1a; color: #e88080; border: 1px solid #603030; }
|
||||
.branch-match { background: #1a2a44; color: #70a8e8; border: 1px solid #2a5080; }
|
||||
.flow-node .sub {
|
||||
display: block;
|
||||
font-size: 0.78rem;
|
||||
color: var(--muted);
|
||||
margin-top: 4px;
|
||||
}
|
||||
/* Filter table */
|
||||
.filter-table {
|
||||
width: 100%;
|
||||
max-width: 540px;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
margin: 0 auto;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.filter-table th {
|
||||
background: #1a2550;
|
||||
color: var(--accent);
|
||||
text-align: left;
|
||||
padding: 10px 16px;
|
||||
font-weight: 700;
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.filter-table td {
|
||||
padding: 9px 16px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.filter-table tr:nth-child(even) td { background: rgba(0,0,0,0.15); }
|
||||
.filter-table tr:nth-child(odd) td { background: rgba(0,0,0,0.08); }
|
||||
.filter-table .result-keep { color: #70d898; }
|
||||
.filter-table .result-fallback { color: #e8c870; }
|
||||
</style>
|
||||
|
||||
<!-- TERMINAL CONNECTION FLOW -->
|
||||
<div class="flow-section">
|
||||
<div class="flow-section-title">Terminal Connection Flow</div>
|
||||
<div class="flow">
|
||||
|
||||
<div class="flow-node node-action">
|
||||
<strong>1.</strong> User runs <code>ao start <project-path></code>
|
||||
</div>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
|
||||
<div class="flow-node node-action">
|
||||
<strong>2.</strong> CLI runtime setup — <code>buildDashboardEnv()</code>
|
||||
<span class="sub">Picks <code>TERMINAL_PORT</code> / <code>DIRECT_TERMINAL_PORT</code> at runtime</span>
|
||||
</div>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
|
||||
<div class="flow-node node-action">
|
||||
<strong>3.</strong> <code>start-all</code> launches servers
|
||||
<span class="sub">Next.js server + direct-terminal-ws on <code>DIRECT_TERMINAL_PORT</code></span>
|
||||
</div>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
|
||||
<div class="flow-node node-action">
|
||||
<strong>4.</strong> Browser opens session page
|
||||
</div>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
|
||||
<div class="flow-node node-decision">
|
||||
<strong>5.</strong> <code>resolveConnectionConfig()</code>
|
||||
<span class="sub">Build-time <code>NEXT_PUBLIC_*</code> values available and valid?</span>
|
||||
</div>
|
||||
<div class="flow-arrow"><div class="arrow-line" style="height:6px"></div></div>
|
||||
|
||||
<div class="flow-branch">
|
||||
<div class="flow-branch-arm">
|
||||
<span class="branch-label branch-yes">Yes</span>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
<div class="flow-node node-success" style="font-size:0.82rem;">
|
||||
Use build-time values directly
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-branch-arm">
|
||||
<span class="branch-label branch-no">No</span>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
<div class="flow-node node-endpoint" style="font-size:0.82rem;">
|
||||
<code>GET /api/runtime/terminal</code>
|
||||
<span class="sub">Read runtime env ports, normalize, return config</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-label">merge</div><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
|
||||
<div class="flow-node node-action">
|
||||
<strong>6.</strong> Client builds WS URL from resolved runtime config
|
||||
</div>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
|
||||
<div class="flow-node node-action">
|
||||
<strong>7.</strong> WebSocket connect to direct-terminal-ws
|
||||
</div>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
|
||||
<div class="flow-node node-action">
|
||||
<strong>8.</strong> Resolve tmux session
|
||||
<span class="sub">Exact match first → hash-prefixed suffix fallback</span>
|
||||
</div>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
|
||||
<div class="flow-node node-success">
|
||||
<strong>9.</strong> PTY attach succeeds — terminal interactive (<code>CONNECTED</code>)
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- SPAWN PATH FLOW -->
|
||||
<div class="flow-section">
|
||||
<div class="flow-section-title">Spawn Path</div>
|
||||
<div class="flow">
|
||||
|
||||
<div class="flow-node node-action">
|
||||
<strong>A.</strong> User triggers spawn
|
||||
<span class="sub">Dashboard / mobile / API caller</span>
|
||||
</div>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
|
||||
<div class="flow-node node-endpoint">
|
||||
<strong>B.</strong> <code>POST /api/spawn</code> with <code>body.projectId</code>
|
||||
</div>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
|
||||
<div class="flow-node node-action">
|
||||
<strong>C.</strong> <code>validateIdentifier(projectId)</code>
|
||||
<span class="sub">Syntax check — format only</span>
|
||||
</div>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
|
||||
<div class="flow-node node-decision">
|
||||
<strong>D.</strong> Semantic guard: <code>config.projects[projectId]</code> exists?
|
||||
</div>
|
||||
<div class="flow-arrow"><div class="arrow-line" style="height:6px"></div></div>
|
||||
|
||||
<div class="flow-branch">
|
||||
<div class="flow-branch-arm">
|
||||
<span class="branch-label branch-yes">Yes</span>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
<div class="flow-node node-action" style="font-size:0.82rem;">
|
||||
<code>sessionManager.spawn()</code>
|
||||
</div>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
<div class="flow-node node-success" style="font-size:0.82rem;">
|
||||
<code>201 Created</code> + session payload
|
||||
<span class="sub">Dashboard/SSE shows active state</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flow-branch-arm">
|
||||
<span class="branch-label branch-no">No</span>
|
||||
<div class="flow-arrow"><div class="arrow-line"></div><div class="arrow-head"></div></div>
|
||||
<div class="flow-node node-error" style="font-size:0.82rem;">
|
||||
<code>404</code> — <code>Unknown project: <id></code>
|
||||
<span class="sub">Stop early — core spawn not invoked</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PROJECT FILTER NORMALIZATION -->
|
||||
<div class="flow-section">
|
||||
<div class="flow-section-title">Project Filter Normalization</div>
|
||||
<div class="flow" style="gap: 12px;">
|
||||
<div class="flow-node node-endpoint" style="margin-bottom:8px;">
|
||||
Incoming <code>?project=<value></code> from dashboard query
|
||||
</div>
|
||||
<table class="filter-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Condition</th>
|
||||
<th>Result</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>value == <code>"all"</code></td>
|
||||
<td class="result-keep">Keep <code>"all"</code></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>value ∈ configured projects</td>
|
||||
<td class="result-keep">Keep value</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>otherwise (e.g. session ID like <code>mono-orchestrator</code>)</td>
|
||||
<td class="result-fallback">Fallback to primary valid project</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p style="font-size:0.82rem; color:var(--muted); text-align:center; margin-top:4px;">
|
||||
Invalid values cannot become active project context.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Alternatives Considered</h2>
|
||||
<ol>
|
||||
<li>Keep fixed ports only (<code>14800/14801</code>) and disable auto-shift. Rejected: blocks multi-instance startup and fails on legitimate port conflicts.</li>
|
||||
<li>Continue relying on <code>NEXT_PUBLIC_*</code> runtime injection. Rejected: production client bundles are build-time materialized.</li>
|
||||
<li>Accept any <code>projectId</code> in <code>/api/spawn</code> and let core throw. Rejected: late 500 errors and poor API ergonomics.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Risks and Mitigations</h2>
|
||||
<ol>
|
||||
<li>Runtime endpoint unavailable. Mitigation: client retains safe fallback and reconnect logic.</li>
|
||||
<li>Reverse-proxy deployments with custom WS path. Mitigation: preserve proxy-path precedence and include runtime proxy field.</li>
|
||||
<li>Behavior change for invalid project query. Mitigation: normalization affects only unknown IDs; valid IDs and <code>all</code> remain unchanged.</li>
|
||||
</ol>
|
||||
|
||||
<h2>Validation Plan</h2>
|
||||
<ul>
|
||||
<li><code>GET /api/runtime/terminal</code> returns runtime env ports.</li>
|
||||
<li><code>POST /api/spawn</code> returns <code>404</code> for unknown project and does not call spawn.</li>
|
||||
<li>Project filter normalization keeps valid IDs, keeps <code>all</code>, and falls back on unknown IDs.</li>
|
||||
<li>Existing <code>DirectTerminal</code> URL construction tests remain green.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Rollout Plan</h2>
|
||||
<ol>
|
||||
<li>Merge patch to main.</li>
|
||||
<li>Release new npm package version containing web/client and API updates.</li>
|
||||
<li>Announce behavior note:
|
||||
<ul>
|
||||
<li>default-port users unaffected</li>
|
||||
<li>runtime-shifted port users no longer hit <code>CONNECTING</code> deadlock</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
<h2>Release Checklist (Commands)</h2>
|
||||
<pre><code># 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</code></pre>
|
||||
|
||||
<h2>Acceptance Criteria</h2>
|
||||
<ol>
|
||||
<li>Direct terminal connects in npm prebuilt flow even when runtime direct port is not <code>14801</code>.</li>
|
||||
<li><code>/api/spawn</code> never returns <code>500</code> for unknown-but-valid-format project IDs.</li>
|
||||
<li>Invalid <code>project</code> query values do not become active project state.</li>
|
||||
<li>Existing default-port flows remain functional without configuration changes.</li>
|
||||
</ol>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,347 +0,0 @@
|
|||
# State Machine Stage 2 Plan: Evidence, Detecting, And Recovery
|
||||
|
||||
Status: Draft
|
||||
Primary issue: #95
|
||||
Target branch: `sessions-redone`
|
||||
Inputs:
|
||||
- `~/.ao/ao-ahead/human-work/redesign.md`
|
||||
- `~/.ao/ao-ahead/aa-2/state-machine-redesign-rollout-plan.md`
|
||||
- `docs/state-machine-redesign.md`
|
||||
- `CLAUDE.md`
|
||||
|
||||
## Purpose
|
||||
|
||||
Stage 2 is where lifecycle classification stops being a pile of loosely competing probes and becomes an explicit evidence assessment system.
|
||||
|
||||
The goal is not to add more statuses. The goal is to make AO answer six questions cleanly and in order:
|
||||
|
||||
1. what evidence exists right now
|
||||
2. how fresh and trustworthy that evidence is
|
||||
3. whether the evidence agrees
|
||||
4. whether the system should enter `detecting`
|
||||
5. whether retries are still allowed
|
||||
6. whether AO should recover automatically, wait for a human, or declare a terminal outcome
|
||||
|
||||
This document is planning only. It defines policy, sequencing, and acceptance criteria for Stage 2. It does not prescribe code-level implementation details yet.
|
||||
|
||||
## Scope
|
||||
|
||||
Stage 2 covers:
|
||||
|
||||
- evidence assessment for runtime, process, activity, and PR-adjacent signals
|
||||
- `detecting` state semantics and exit rules
|
||||
- signal disagreement classification
|
||||
- stale evidence policy by source type
|
||||
- bounded retry policy for uncertain or failed probes
|
||||
- recovery rules for worker and orchestrator sessions
|
||||
- decision logging needed to explain why AO chose a state
|
||||
|
||||
Stage 2 does not cover:
|
||||
|
||||
- agent-authored lifecycle reporting commands
|
||||
- prompt changes to force explicit acknowledgment or waiting signals
|
||||
- UI redesign beyond the minimum data contracts Stage 2 requires
|
||||
- full PR workflow automation redesign
|
||||
|
||||
## Design Position
|
||||
|
||||
Stage 2 should treat runtime truth, process truth, activity truth, and PR truth as separate inputs to an assessment pass. No single weak signal should be allowed to kill a session. `detecting` is the controlled buffer state used when the system cannot yet defend a confident answer.
|
||||
|
||||
The practical policy is:
|
||||
|
||||
- prefer explicit facts over heuristics
|
||||
- prefer `detecting` over a false terminal label
|
||||
- prefer bounded waiting over infinite ambiguity
|
||||
- prefer recovery when the session is plausibly resumable
|
||||
- prefer human escalation when the evidence is conflicting after retries
|
||||
|
||||
## Evidence Inventory
|
||||
|
||||
Each assessment pass should normalize evidence into a common shape with `source`, `observedAt`, `freshness`, `reliability`, and `value`.
|
||||
|
||||
### Source classes
|
||||
|
||||
- runtime evidence
|
||||
- tmux/session handle exists
|
||||
- runtime plugin reports reachable or unreachable
|
||||
- process evidence
|
||||
- agent process scan reports running or not running
|
||||
- pid metadata exists or is missing
|
||||
- activity evidence
|
||||
- structured agent activity
|
||||
- AO-generated activity markers
|
||||
- terminal output heartbeat only as weak fallback
|
||||
- recovery evidence
|
||||
- provider-specific session resumability
|
||||
- known recovery action available
|
||||
- PR evidence
|
||||
- PR open, merged, closed, CI pending, CI failing, review comments
|
||||
- operator evidence
|
||||
- explicit user kill
|
||||
- explicit user continue/resume request
|
||||
|
||||
### Reliability tiers
|
||||
|
||||
- `authoritative`
|
||||
- explicit user kill
|
||||
- successful runtime/plugin confirmation
|
||||
- successful provider resumability check
|
||||
- `strong`
|
||||
- process running or not running from a validated probe
|
||||
- fresh structured activity from AO or provider logs
|
||||
- `supporting`
|
||||
- recent terminal output without structured semantics
|
||||
- existing metadata from prior successful scans
|
||||
- `historical`
|
||||
- stale activity
|
||||
- previous assessment results
|
||||
|
||||
## Assessment Model
|
||||
|
||||
Every lifecycle pass should produce an evidence report before it produces a session state.
|
||||
|
||||
### Pass order
|
||||
|
||||
1. collect the latest evidence for runtime, process, activity, recovery, and PR
|
||||
2. grade each evidence item for freshness and reliability
|
||||
3. detect contradictions
|
||||
4. decide whether the contradiction is resolvable within retry bounds
|
||||
5. emit one of:
|
||||
- confident classification
|
||||
- `detecting`
|
||||
- terminal outcome with explicit reason
|
||||
- recovery-needed classification
|
||||
|
||||
### Required outputs
|
||||
|
||||
Each pass should produce:
|
||||
|
||||
- session assessment outcome
|
||||
- primary reason
|
||||
- evidence summary
|
||||
- disagreement summary if any
|
||||
- retry counter state
|
||||
- recovery recommendation if any
|
||||
|
||||
## `detecting` State Semantics
|
||||
|
||||
`detecting` is not a synonym for `unknown`. It is an active assessment window where AO is intentionally trying to resolve conflicting, missing, or failed evidence.
|
||||
|
||||
### Enter `detecting` when
|
||||
|
||||
- runtime and process signals disagree
|
||||
- runtime is unreachable but fresh activity still exists
|
||||
- process probe fails in a way that could be transient
|
||||
- activity is recent enough to block a dead classification but not enough to confirm health
|
||||
- recovery appears possible but the recovery preconditions are not yet confirmed
|
||||
- required probes time out or return incomplete data
|
||||
|
||||
### Do not enter `detecting` when
|
||||
|
||||
- user kill is explicit and confirmed
|
||||
- runtime and process are both confirmed dead and no recovery path exists
|
||||
- session is intentionally terminal for a reason independent of liveness
|
||||
- the same disagreement has already exhausted the retry budget
|
||||
|
||||
### Exit `detecting` when
|
||||
|
||||
- evidence converges on a confident non-terminal state
|
||||
- evidence converges on a terminal outcome
|
||||
- bounded retries are exhausted and the result must be escalated
|
||||
- recovery succeeds and a new healthy state is established
|
||||
- recovery is impossible and the terminal reason is clear
|
||||
|
||||
## Signal Disagreement Handling
|
||||
|
||||
Disagreement handling must be explicit. Stage 2 should classify disagreements instead of burying them inside ad hoc fallthrough logic.
|
||||
|
||||
### Core disagreement classes
|
||||
|
||||
- `runtime_alive_process_dead`
|
||||
- `runtime_dead_process_alive`
|
||||
- `runtime_dead_recent_activity`
|
||||
- `process_alive_no_recent_activity`
|
||||
- `probe_failed_runtime_unknown`
|
||||
- `probe_failed_process_unknown`
|
||||
- `pr_terminal_runtime_alive`
|
||||
- `recovery_possible_runtime_uncertain`
|
||||
|
||||
### Resolution policy
|
||||
|
||||
- `runtime_alive_process_dead`
|
||||
- treat as likely recoverable worker failure
|
||||
- enter `detecting`
|
||||
- retry process probe
|
||||
- evaluate resumability before declaring terminal
|
||||
- `runtime_dead_process_alive`
|
||||
- treat process evidence as suspicious
|
||||
- prefer `detecting`
|
||||
- retry runtime probe
|
||||
- downgrade process evidence if the runtime container/session cannot be confirmed
|
||||
- `runtime_dead_recent_activity`
|
||||
- treat stale-vs-live timing carefully
|
||||
- recent structured activity blocks immediate death
|
||||
- historical activity does not
|
||||
- `process_alive_no_recent_activity`
|
||||
- do not classify as dead
|
||||
- remain in current workflow state or `idle`/`stuck` depending on broader context
|
||||
- `pr_terminal_runtime_alive`
|
||||
- do not kill solely because the PR state is terminal
|
||||
- classify workflow and runtime separately
|
||||
|
||||
## Stale Evidence Policy
|
||||
|
||||
Stale evidence should explain history, not overrule current hard facts.
|
||||
|
||||
### Freshness windows
|
||||
|
||||
- structured activity
|
||||
- `0s-60s`: strong support for liveness
|
||||
- `61s-5m`: weak support for liveness, enough to block immediate death on its own
|
||||
- `>5m`: historical only
|
||||
- terminal heartbeat output
|
||||
- `0s-30s`: supporting only
|
||||
- `31s-2m`: weak historical context
|
||||
- `>2m`: ignore for liveness decisions
|
||||
- runtime/process probe results
|
||||
- `0s-30s`: current
|
||||
- `31s-90s`: usable but should be refreshed before a terminal classification
|
||||
- `>90s`: stale for decisive outcomes
|
||||
- PR state
|
||||
- remains authoritative for PR truth
|
||||
- does not become authoritative for runtime death
|
||||
|
||||
### Rules
|
||||
|
||||
- stale activity cannot by itself prove the session is alive
|
||||
- stale activity can explain why AO avoided a dead classification earlier
|
||||
- a terminal outcome requires current hard evidence or an explicit operator action
|
||||
- recovery eligibility may use stale evidence only to rank options, not to assert that recovery is safe
|
||||
|
||||
## Bounded Retry Policy
|
||||
|
||||
`detecting` must be temporary. Every disagreement path needs a fixed retry budget and an explicit escalation outcome.
|
||||
|
||||
### Retry budgets
|
||||
|
||||
- transient probe failures
|
||||
- retry up to 3 times
|
||||
- exponential spacing such as immediate, 10s, 30s
|
||||
- hard disagreement with partial evidence
|
||||
- retry up to 2 full assessment passes after the initial pass
|
||||
- recovery attempt verification
|
||||
- allow 1 recovery attempt per assessment cycle unless the operator explicitly asks again
|
||||
|
||||
### Retry stop conditions
|
||||
|
||||
- decisive evidence arrives
|
||||
- the retry budget is exhausted
|
||||
- an operator action overrides automated handling
|
||||
- the recovery subsystem returns a definitive non-recoverable result
|
||||
|
||||
### Escalation after retries
|
||||
|
||||
When retries are exhausted, AO should not loop forever. It should produce one of:
|
||||
|
||||
- `stuck` with a clear reason if human action is needed
|
||||
- terminal with explicit reason if death is defensible
|
||||
- recovery-pending-human if recovery exists but is unsafe to auto-run
|
||||
|
||||
## Recovery Rules
|
||||
|
||||
Recovery must be policy-driven, not implicit.
|
||||
|
||||
### Worker sessions
|
||||
|
||||
Auto-recovery is allowed when:
|
||||
|
||||
- the runtime is reachable or can be recreated safely
|
||||
- the provider session is resumable or a new worker can be created with preserved context
|
||||
- the failure reason is operational rather than semantic
|
||||
- there is no explicit user kill or explicit human stop condition
|
||||
|
||||
Human-gated recovery is required when:
|
||||
|
||||
- the evidence is still contradictory after retries
|
||||
- PR state suggests the worker should be preserved rather than replaced
|
||||
- the replacement action could duplicate work or create PR confusion
|
||||
- permissions or credentials are required
|
||||
|
||||
Do not auto-recover when:
|
||||
|
||||
- the user explicitly killed the session
|
||||
- the provider reports the session as non-resumable and replacement would violate workflow policy
|
||||
- the session is already intentionally terminal
|
||||
|
||||
### Orchestrator sessions
|
||||
|
||||
Orchestrators should have a higher bar for terminal classification and auto-recovery attempts because they coordinate other sessions.
|
||||
|
||||
Policy:
|
||||
|
||||
- prefer recovery over terminal classification
|
||||
- require stronger evidence before declaring them dead
|
||||
- never tie orchestrator death to PR state
|
||||
- surface unresolved orchestrator disagreements prominently for human review
|
||||
|
||||
## Recovery Outcome Matrix
|
||||
|
||||
### Recover automatically
|
||||
|
||||
- worker runtime reachable, process dead, resumable session available
|
||||
- worker runtime reachable, process dead, replacement worker policy explicitly allows restart
|
||||
|
||||
### Wait for human
|
||||
|
||||
- retries exhausted with unresolved disagreement
|
||||
- PR merged or closed but runtime is still alive and policy choice is needed
|
||||
- recovery would create a second worker or reopen a completed workflow
|
||||
|
||||
### Declare terminal
|
||||
|
||||
- explicit user kill confirmed
|
||||
- runtime and process both confirmed dead after fresh probes
|
||||
- recovery path explicitly unavailable and workflow policy allows termination
|
||||
|
||||
## Decision Logging Requirements
|
||||
|
||||
Stage 2 needs durable reasoning trails so the dashboard and operators can understand why a status changed.
|
||||
|
||||
Each assessment result should record:
|
||||
|
||||
- prior session state and reason
|
||||
- new session state and reason
|
||||
- major evidence items considered
|
||||
- freshness classification for each decisive signal
|
||||
- disagreement code if one existed
|
||||
- retry count and remaining budget
|
||||
- recovery action attempted, skipped, or blocked
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
Stage 2 is ready when the plan can support these guarantees:
|
||||
|
||||
- AO no longer declares a session dead from one weak signal
|
||||
- `detecting` has defined entry and exit conditions
|
||||
- every disagreement class has a retry policy and an escalation outcome
|
||||
- stale evidence has explicit freshness windows by source type
|
||||
- worker and orchestrator recovery policies differ where operationally necessary
|
||||
- every terminal outcome has a defendable reason
|
||||
- operators can inspect why AO chose the current state
|
||||
|
||||
## Suggested Implementation Sequence
|
||||
|
||||
1. define evidence data shapes and disagreement codes
|
||||
2. define freshness grading and stale evidence rules
|
||||
3. extract lifecycle assessment into a dedicated evaluation step
|
||||
4. wire `detecting` entry, exit, and retry counters
|
||||
5. define recovery policy gates for worker versus orchestrator sessions
|
||||
6. add decision logging and test scenarios for each disagreement class
|
||||
|
||||
## Open Decisions To Confirm Before Coding
|
||||
|
||||
- whether the activity freshness windows should differ by agent provider
|
||||
- whether terminal output should be considered at all once structured activity exists
|
||||
- whether worker replacement and session resume are separate recovery classes in Stage 2 or deferred to Stage 3
|
||||
- whether retry counters live in session metadata or are recomputed from assessment history
|
||||
- whether `stuck` is emitted directly by Stage 2 or remains a later projection over assessment outcomes
|
||||
Loading…
Reference in New Issue