diff --git a/.codex-synaptic/memory.db b/.codex-synaptic/memory.db index 8308945..4e45cb9 100644 Binary files a/.codex-synaptic/memory.db and b/.codex-synaptic/memory.db differ diff --git a/.github/workflows/ci-non-mcp-gates.yml b/.github/workflows/ci-non-mcp-gates.yml new file mode 100644 index 0000000..df981a4 --- /dev/null +++ b/.github/workflows/ci-non-mcp-gates.yml @@ -0,0 +1,64 @@ +name: CI Non-MCP Gates + +on: + push: + pull_request: + workflow_dispatch: + +concurrency: + group: ci-non-mcp-gates-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + non_mcp_gates: + name: Build, Test, Lint, Preflight + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: read + env: + CI: "true" + CODEX_AUTO_LINK: "false" + steps: + - name: Checkout repository + uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "20" + cache: npm + + - name: Install dependencies + run: | + set -euo pipefail + npm ci + + - name: Build + run: | + set -euo pipefail + npm run build + + - name: Test + env: + HOME: ${{ runner.temp }}/codex-synaptic-ci-home + run: | + set -euo pipefail + mkdir -p "${HOME}" + npm test + + - name: Lint + run: | + set -euo pipefail + npm run lint + + - name: Release preflight (canonical repo) + if: github.repository == 'clduab11/codex-synaptic' + run: | + set -euo pipefail + npm run release:preflight + + - name: Release preflight skipped (non-canonical repo) + if: github.repository != 'clduab11/codex-synaptic' + run: | + echo "Skipping release preflight: repository is ${{ github.repository }} (expects clduab11/codex-synaptic)." diff --git a/AGENTS.md b/AGENTS.md index ac5cfd7..02ad9b0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,6 +10,13 @@ The Codex-Synaptic system enhances OpenAI's Codex with advanced multi-agent capa - **Autoscaler behaviour:** With the background daemon disabled, idle worker retirement requests cannot execute. Expect scale-down warnings in logs and manually right-size replicas after experiments. See `docs/runbooks/autoscaler-daemon-coordination.md` for operational guidance. - **Repository hygiene:** Active development is running from the local `codex-synaptic-clone` directory, but upstream pushes must target `github.com/clduab11/codex-synaptic`. Align the folder/remote names before release packaging so automation recipes resolve assets correctly. See `docs/runbooks/workspace-rename-guide.md` for the step-by-step procedure. +## Startup Gate (Codex For macOS) + +- Run `codex-synaptic launch --json` before repository work whenever the user asks to launch or verify readiness first. +- Treat launch as a hard gate: if `ok` is `false` (or the command exits non-zero), stop and only return remediation commands. +- Proceed with repository changes only when launch returns `ok=true` and `nextAction="continue"`. +- Default launch gate profiles are `mcp-filesystem`, `mcp-playwright`, and `mcp-desktop-commander`. + ## Core Agent Types ### 1. Worker Agents diff --git a/README.md b/README.md index 4190d1d..03dd443 100644 --- a/README.md +++ b/README.md @@ -59,8 +59,10 @@ npm install npm run build # readiness +node dist/cli/index.js launch --json +node dist/cli/index.js launch --strict --json node dist/cli/index.js doctor -node dist/cli/index.js doctor --strict +node dist/cli/index.js doctor --strict --json # daemon lifecycle node dist/cli/index.js background start @@ -76,6 +78,7 @@ node dist/cli/index.js tui --local --interval 1000 # MCP profiles and registration node dist/cli/index.js env plan mcp-filesystem mcp-playwright mcp-desktop-commander +node dist/cli/index.js env docker-login mcp-filesystem mcp-playwright mcp-desktop-commander node dist/cli/index.js env up mcp-filesystem mcp-playwright mcp-desktop-commander node dist/cli/index.js env status mcp-filesystem mcp-playwright mcp-desktop-commander node dist/cli/index.js env codex-register mcp-filesystem mcp-playwright mcp-desktop-commander --replace @@ -89,6 +92,9 @@ Non-interactive CLI commands run in one-shot mode by default (the process exits # Local mode codex -C /absolute/path/to/codex-synaptic +# first-launch gate in this repo +codex-synaptic launch --json + # Worktree mode git worktree add ../codex-synaptic-worktree -b codex/macos-ops codex -C ../codex-synaptic-worktree diff --git a/docker/mcp/docker-compose.playwright.yml b/docker/mcp/docker-compose.playwright.yml index 0abd428..07049f1 100644 --- a/docker/mcp/docker-compose.playwright.yml +++ b/docker/mcp/docker-compose.playwright.yml @@ -1,10 +1,10 @@ version: '3.8' services: mcp-playwright: - image: ghcr.io/context-labs/playwright-mcp:latest + image: mcp/playwright:latest container_name: codex-mcp-playwright restart: unless-stopped ports: - "7030:7030" shm_size: '1gb' - command: ["--port", "7030"] + command: ["--port", "7030", "--host", "0.0.0.0"] diff --git a/docs/guides/codex-macos-workflows.md b/docs/guides/codex-macos-workflows.md index 527a2d4..f8f9b56 100644 --- a/docs/guides/codex-macos-workflows.md +++ b/docs/guides/codex-macos-workflows.md @@ -1,6 +1,6 @@ # Codex macOS Workflows (Local, Worktree, Cloud + MCP) -Last reviewed: 2026-02-13 +Last reviewed: 2026-02-14 Audience: contributors using Codex app/CLI on macOS (Apple Silicon) with Codex-Synaptic. ## Source Of Truth @@ -17,7 +17,7 @@ This guide is aligned with: - `https://developers.openai.com/codex/cli/features/` - `https://developers.openai.com/codex/security/` -## Bootstrap And Doctor (Run First) +## Bootstrap And Launch Gate (Run First) ```bash cd /absolute/path/to/codex-synaptic @@ -29,11 +29,40 @@ codex --help codex mcp --help codex mcp add --help -# one-shot readiness checks (auth + mcp + repo cli) -node dist/cli/index.js doctor +# one-command bootstrap + strict readiness gate +node dist/cli/index.js launch --json -# enforce failure in CI/automation -node dist/cli/index.js doctor --strict --json +# explicit strict form for CI/automation +node dist/cli/index.js launch --strict --json +``` + +Launch defaults: + +- Detached runtime authority (`background start`) that remains running after success. +- Required MCP gate set: `mcp-filesystem`, `mcp-playwright`, `mcp-desktop-commander`. +- Hard-stop behavior in strict mode: first failing gate exits non-zero with remediation commands. + +Typical first Codex app prompt in this repo: + +```text +Launch codex-synaptic and determine health/status prior to beginning repository work. +``` + +### Launch Failure Remediation Examples + +```bash +# Codex auth missing +codex login + +# Docker registry auth for MCP images +node dist/cli/index.js env docker-login mcp-filesystem mcp-playwright mcp-desktop-commander + +# MCP runtime or registration drift +node dist/cli/index.js env up mcp-filesystem mcp-playwright mcp-desktop-commander +node dist/cli/index.js env codex-register mcp-filesystem mcp-playwright mcp-desktop-commander --replace + +# Re-run hard gate +node dist/cli/index.js launch --strict --json ``` ## Runtime Model (Deterministic) @@ -130,6 +159,9 @@ codex cloud apply # inspect profiles and codex registration targets node dist/cli/index.js env plan mcp-filesystem mcp-playwright mcp-desktop-commander +# authenticate required Docker registries (for private GHCR images) +node dist/cli/index.js env docker-login mcp-filesystem mcp-playwright mcp-desktop-commander + # safest default: filesystem read-only node dist/cli/index.js env up mcp-filesystem mcp-playwright mcp-desktop-commander @@ -147,6 +179,7 @@ codex mcp list --json Expected indicators: - `env status` returns `running: yes` and `healthy: yes` for active profiles. +- `launch --json` returns `ok: true` and `nextAction: "continue"`. - `doctor` reports MCP profile checks passing and registration present. ## Sandbox And Approval Recommendations @@ -169,7 +202,7 @@ codex --sandbox read-only --ask-for-approval on-request ```bash # 1) refresh build + readiness npm run build -node dist/cli/index.js doctor --strict +node dist/cli/index.js launch --strict --json # 2) run focused work codex exec "Implement one bounded fix with tests" diff --git a/docs/guides/quick-start.md b/docs/guides/quick-start.md index 5fbc20b..54fa862 100644 --- a/docs/guides/quick-start.md +++ b/docs/guides/quick-start.md @@ -1,6 +1,6 @@ # Quick Start (Codex-Synaptic + Codex macOS) -Last reviewed: 2026-02-10 +Last reviewed: 2026-02-14 ## 1. Install and build @@ -9,29 +9,35 @@ npm install npm run build ``` -## 2. Verify CLI health +## 2. Run launch gate ```bash -npm run cli -- system status +npm run cli -- launch --strict --json ``` -Expected output in a cold shell: +Expected success indicators: -```text -System not started. Run `codex-synaptic system start` first. +```json +{ + "ok": true, + "nextAction": "continue" +} ``` -Expected output after startup: +If launch fails, stop repository work and run the remediation commands returned in the report. +For Docker registry-denied errors, run: ```bash -npm run cli -- system start +npm run cli -- env docker-login mcp-filesystem mcp-playwright mcp-desktop-commander ``` -This command prints a telemetry snapshot and then exits cleanly in one-shot mode. +## 3. Optional direct runtime inspection -If you need to keep the foreground process alive for debugging, run with `CODEX_CLI_AUTO_SHUTDOWN=0`. +```bash +npm run cli -- system status +``` -## 3. Run a minimal local workflow +## 4. Run a minimal local workflow ```bash npm run cli -- reasoning plan "Stabilize codex-synaptic release readiness" --require-consensus --json @@ -39,13 +45,13 @@ npm run cli -- openai usage --json npm run cli -- hive-mind spawn "Verify macOS readiness smoke flow" --codex --dry-run ``` -## 4. Use Codex passthrough +## 5. Use Codex passthrough ```bash codex-synaptic --codex --dry-run "Inspect current readiness blockers and propose bounded fixes" ``` -## 5. Run verification gates +## 6. Run verification gates ```bash npm run lint diff --git a/docs/uat/CODEX_MACOS_UAT_RUNBOOK.md b/docs/uat/CODEX_MACOS_UAT_RUNBOOK.md new file mode 100644 index 0000000..3e58f22 --- /dev/null +++ b/docs/uat/CODEX_MACOS_UAT_RUNBOOK.md @@ -0,0 +1,364 @@ +# Codex for macOS UAT Runbook (Launch/Doctor MCP Readiness) + +Last updated: 2026-02-24 +Audience: UAT operators validating Codex for macOS + `codex-synaptic` launch readiness. +Scope: UAT readiness only (not PRD release readiness). + +## Purpose + +Provide a deterministic, testable procedure for validating the Codex for macOS startup path in this repository, with `launch` and `doctor` as hard readiness gates for default MCP profiles: + +- `mcp-filesystem` +- `mcp-playwright` +- `mcp-desktop-commander` + +This runbook is aligned with: + +- `AGENTS.md` (Startup Gate semantics) +- `README.md` (operator command deck + MCP workflow) +- `docs/guides/codex-macos-workflows.md` +- `docs/mcp/README.md` + +## UAT Pass Criteria (Top-Level) + +UAT launch readiness is considered `PASS` only when all of the following are true: + +1. `node dist/cli/index.js doctor --strict --json` exits `0` and returns JSON with: + - `"ok": true` + - `"summary.failed": 0` + - passing checks for default MCP profiles (`mcp.mcp-filesystem`, `mcp.mcp-playwright`, `mcp.mcp-desktop-commander`) +2. `node dist/cli/index.js launch --strict --json` exits `0` and returns JSON with: + - `"ok": true` + - `"nextAction": "continue"` + - all launch steps `ok=true` including `mcp.up`, `mcp.codex_register`, and `doctor.strict` +3. Default `mcp-filesystem` behavior remains read-only unless explicitly opted into controlled write. + +## UAT Environment Prerequisites + +Do not start the run until these are confirmed: + +- macOS host with Docker Desktop installed and running +- Node.js/npm installed (repo uses Node 20+) +- Codex CLI installed and on `PATH` (`codex --help`) +- Codex CLI authenticated (`codex login status`) +- Network access to pull MCP images +- Docker registry credentials for any private registries still referenced by the active MCP profile images + +Notes: + +- CLI local `.env` autoload is enabled by default. Set `CODEX_CLI_ENV_AUTOLOAD=0` to disable auto-loading for UAT runs. +- For non-JSON commands, the CLI may emit an env bootstrap banner to `stderr` (not `stdout`). Treat local env source details as sensitive operational context. +- Set `CODEX_CLI_ENV_BANNER_VERBOSE=1` only when debugging env source paths; avoid using it in shared logs/screenshots. +- Do not capture or paste secrets from `codex login`, Docker login prompts, or local `.env` files. + +### Upstream Image Migration Note (2026-02-24) + +Verified against upstream project documentation and live registry pulls: + +- `mcp-playwright` canonical image is now the Docker Hub MCP image `mcp/playwright:latest` (also mirrored from official Playwright MCP Docker guidance). +- `mcp-filesystem` canonical image is `mcp/filesystem:latest`, but it is a stdio-oriented MCP server image (not a drop-in HTTP/`--port` replacement for this repo's current `env up` + `codex mcp add --url` flow). +- `mcp-desktop-commander` canonical image is `mcp/desktop-commander:latest`, but it is also stdio-oriented and does not accept `--port`. +- Legacy GHCR wrapper images previously used by this repo (for example `ghcr.io/context-labs/*-mcp` and `ghcr.io/wonderwhy-er/desktop-commander`) now return registry `404` (not found), even with authenticated GHCR access. + +Implication for UAT: + +- Updating image references alone only cleanly unblocks `mcp-playwright`. +- `mcp-filesystem` and `mcp-desktop-commander` require either: + - replacement HTTP-capable wrapper images, or + - a profile/registration redesign to use stdio MCP registration instead of HTTP URL registration. + +## Recommended Evidence Capture (Optional but Strongly Recommended) + +Create a dated folder and store JSON outputs for handoff: + +```bash +mkdir -p docs/uat/evidence/2026-02-23 +``` + +Suggested evidence artifacts: + +- `doctor.strict.json` +- `launch.strict.json` +- `codex.mcp.list.json` +- `env.status.txt` + +## UAT Procedure (Exact Commands) + +Run all commands from the repo root: + +```bash +cd /absolute/path/to/codex-synaptic +``` + +### 1) Build the CLI artifacts + +```bash +npm install +npm run build +``` + +Pass criteria: + +- `npm install` completes without fatal errors +- `npm run build` exits `0` +- `dist/cli/index.js` exists + +### 2) Verify Codex CLI + MCP command surface + +```bash +codex --help +codex mcp --help +codex mcp add --help +codex login status +``` + +Pass criteria: + +- All commands exit `0` +- `codex login status` indicates logged-in state (not "not logged in") + +### 3) Inspect default MCP profiles and registration targets + +```bash +node dist/cli/index.js env plan mcp-filesystem mcp-playwright mcp-desktop-commander +``` + +Pass criteria: + +- Output includes all 3 profile names +- Output shows expected Codex MCP names: + - `filesystem-local` + - `playwright-local` + - `desktop-commander` +- `mcp-filesystem` notes read-only default mode + +### 4) Authenticate Docker registry access for MCP images + +```bash +node dist/cli/index.js env docker-login mcp-filesystem mcp-playwright mcp-desktop-commander +``` + +Pass criteria: + +- Command exits `0` +- Docker login succeeds for required private registries (if any are required by the active profile images) +- Public Docker Hub `mcp/*` images generally do not require `docker login` + +Common failure indicators (blockers): + +- `error from registry: denied` +- `pull access denied` +- `unauthorized` +- `failed to resolve reference ... : not found` (image/repository/tag drift or deprecation) + +### 5) Start default MCP profiles (read-only filesystem mode by default) + +```bash +node dist/cli/index.js env up mcp-filesystem mcp-playwright mcp-desktop-commander +``` + +Pass criteria: + +- Command exits `0` +- No healthcheck timeout errors +- Filesystem profile starts without controlled-write flags (default safe mode) + +### 6) Verify MCP runtime health + +```bash +node dist/cli/index.js env status mcp-filesystem mcp-playwright mcp-desktop-commander +``` + +Pass criteria for each profile: + +- `running: yes` +- `healthy: yes` (or probe-equivalent success) +- no blocking diagnostics + +If collecting evidence: + +```bash +node dist/cli/index.js env status mcp-filesystem mcp-playwright mcp-desktop-commander | tee docs/uat/evidence/2026-02-23/env.status.txt +``` + +### 7) Register MCP HTTP endpoints with Codex + +```bash +node dist/cli/index.js env codex-register mcp-filesystem mcp-playwright mcp-desktop-commander --replace +codex mcp list --json +``` + +Pass criteria: + +- Registration command exits `0` +- `codex mcp list --json` exits `0` +- JSON includes all names: + - `filesystem-local` + - `playwright-local` + - `desktop-commander` + +If collecting evidence: + +```bash +codex mcp list --json > docs/uat/evidence/2026-02-23/codex.mcp.list.json +``` + +### 8) Run strict doctor gate (authoritative diagnostic pass) + +```bash +node dist/cli/index.js doctor --strict --json +``` + +Pass criteria: + +- Exit code `0` +- JSON contains: + - `"ok": true` + - `"summary": { "failed": 0, ... }` +- Checks include and pass: + - `repo.cli_build_artifact` + - `repo.cli_exec` + - `codex.auth` + - `codex.mcp_list` + - `mcp.mcp-filesystem` + - `mcp.mcp-playwright` + - `mcp.mcp-desktop-commander` + +If collecting evidence: + +```bash +node dist/cli/index.js doctor --strict --json > docs/uat/evidence/2026-02-23/doctor.strict.json +``` + +### 9) Run strict launch gate (hard startup gate) + +```bash +node dist/cli/index.js launch --strict --json +``` + +Pass criteria: + +- Exit code `0` +- JSON contains: + - `"ok": true` + - `"nextAction": "continue"` +- `steps` contains all required launch gate steps with `"ok": true`: + - `repo.preflight` + - `codex.auth` + - `runtime.daemon` + - `mcp.up` + - `mcp.codex_register` + - `doctor.strict` +- `doctor.ok` is `true` + +If collecting evidence: + +```bash +node dist/cli/index.js launch --strict --json > docs/uat/evidence/2026-02-23/launch.strict.json +``` + +## Deterministic Fail/Block Conditions + +Mark UAT launch readiness `FAIL` (or `BLOCKED`) if any of the following occur: + +- `launch --strict --json` exits non-zero +- `launch` JSON returns `"ok": false` or `"nextAction": "stop"` +- `doctor --strict --json` exits non-zero +- `doctor` JSON returns `"ok": false` or any failed default MCP check +- Any default MCP profile is not running/healthy/registered +- Docker registry auth/pull denial prevents `mcp.up` (for example GHCR denial) +- Codex auth not available (`codex login status` not logged in) + +## Failure Triage (Fast Path) + +Use the failure message/remediation emitted by `launch`/`doctor`. Common paths: + +### A. Docker image pull/auth denied (GHCR) + +Symptom examples: + +- `error from registry: denied` +- `pull access denied` + +Remediation: + +```bash +node dist/cli/index.js env docker-login mcp-filesystem mcp-playwright mcp-desktop-commander +node dist/cli/index.js env up mcp-filesystem mcp-playwright mcp-desktop-commander +``` + +Then retry: + +```bash +node dist/cli/index.js doctor --strict --json +node dist/cli/index.js launch --strict --json +``` + +### B. MCP registration drift + +Symptom: + +- `doctor` shows `registered=false` for one or more MCP checks + +Remediation: + +```bash +node dist/cli/index.js env codex-register mcp-filesystem mcp-playwright mcp-desktop-commander --replace +codex mcp list --json +``` + +### C. Service running but unhealthy / health timeout + +Symptom: + +- `healthy=false` +- `Service healthcheck timed out ...` + +Remediation: + +```bash +node dist/cli/index.js env status mcp-filesystem mcp-playwright mcp-desktop-commander +node dist/cli/index.js env up mcp-filesystem mcp-playwright mcp-desktop-commander +``` + +If persistent, capture `env status` output and treat as UAT blocker. + +### D. Codex auth missing + +Symptom: + +- `codex.auth` fails in `doctor`/`launch` + +Remediation: + +```bash +codex login +codex login status +``` + +## UAT Acceptance Checklist (Operator Sign-Off) + +Use this checklist during the run and archive it with evidence: + +- [ ] Running on macOS UAT host with Docker Desktop active +- [ ] `npm install` completed successfully +- [ ] `npm run build` completed successfully +- [ ] `codex login status` confirms logged-in state +- [ ] `env plan` confirms default MCP profiles and expected Codex registration names +- [ ] `env docker-login` completed successfully (registry access verified) +- [ ] `env up` started `mcp-filesystem`, `mcp-playwright`, `mcp-desktop-commander` +- [ ] `env status` shows `running: yes` and `healthy: yes` for all default MCP profiles +- [ ] `env codex-register ... --replace` completed successfully +- [ ] `codex mcp list --json` includes `filesystem-local`, `playwright-local`, `desktop-commander` +- [ ] `doctor --strict --json` exits `0` with `"ok": true` +- [ ] `launch --strict --json` exits `0` with `"ok": true` and `"nextAction": "continue"` +- [ ] No secrets captured in shared evidence/logs/screenshots + +## Out of Scope for This UAT Runbook + +This runbook validates launch/doctor MCP readiness only. It does not certify: + +- CI/CD release pipeline completeness +- package publication scope hardening +- dependency vulnerability remediation +- full beta/PRD readiness criteria from `docs/beta-readiness-checklist.md` diff --git a/docs/uat/UAT_FINAL_REPORT.md b/docs/uat/UAT_FINAL_REPORT.md new file mode 100644 index 0000000..b09a6a3 --- /dev/null +++ b/docs/uat/UAT_FINAL_REPORT.md @@ -0,0 +1,141 @@ +# Codex for macOS UAT Final Report (Chunk 7 Rerun) + +Date: 2026-02-24 +Repository: `codex-synaptic` +Branch: `codex/uat-macos-readiness` +Scope: Codex for macOS launch/doctor MCP readiness (UAT, not PRD) + +## Final Verdict + +Status: `BLOCKED` + +Primary blocker (environment/image availability): + +- GHCR authentication is now present on the UAT host (interactive login completed; `env docker-login` succeeds), but the default MCP image references currently used by the launch gate resolve to `not found`. +- This prevents `env up` and therefore blocks both `doctor --strict --json` and `launch --strict --json` from passing. + +Code/output-contract status (previously blocking, now resolved): + +- `launch --strict --json` stdout purity has been fixed. +- In the rerun evidence, `launch.strict.json` is valid JSON directly (no stdout prefix contamination / no post-processing required). + +## Evidence Location + +Primary rerun evidence (authoritative for this report): + +- `/Users/chrisdukes/LocalProjects/codex-synaptic/docs/uat/evidence/2026-02-24-rerun-1/` +- Step matrix: `/Users/chrisdukes/LocalProjects/codex-synaptic/docs/uat/evidence/2026-02-24-rerun-1/_status.tsv` + +Prior blocked run (preserved for traceability): + +- `/Users/chrisdukes/LocalProjects/codex-synaptic/docs/uat/evidence/2026-02-24/` + +## Smoke Run Results (Runbook Path, Rerun) + +### Command matrix (steps 1-13) + +| Step | Command | Exit | Result | +| ---- | --------------------------------------------------------------------------------------------------------- | ---: | ------ | +| 01 | `CODEX_AUTO_LINK=false npm run build` | 0 | PASS | +| 02 | `codex --help` | 0 | PASS | +| 03 | `codex mcp --help` | 0 | PASS | +| 04 | `codex mcp add --help` | 0 | PASS | +| 05 | `codex login status` | 0 | PASS (`Logged in using ChatGPT`) | +| 06 | `node dist/cli/index.js env plan mcp-filesystem mcp-playwright mcp-desktop-commander` | 0 | PASS | +| 07 | `node dist/cli/index.js env docker-login mcp-filesystem mcp-playwright mcp-desktop-commander` | 0 | PASS (Docker reused existing GHCR credentials; `Login Succeeded`) | +| 08 | `node dist/cli/index.js env up mcp-filesystem mcp-playwright mcp-desktop-commander` | 1 | BLOCKED (default MCP image reference not found) | +| 09 | `node dist/cli/index.js env status mcp-filesystem mcp-playwright mcp-desktop-commander` | 0 | PASS (profiles reported not running/not healthy after step 08 failure) | +| 10 | `node dist/cli/index.js env codex-register mcp-filesystem mcp-playwright mcp-desktop-commander --replace` | 0 | PASS | +| 11 | `codex mcp list --json` | 0 | PASS (all 3 required MCP names present; artifact redacted) | +| 12 | `node dist/cli/index.js doctor --strict --json` | 1 | BLOCKED (3 default MCP profile checks failed as expected) | +| 13 | `node dist/cli/index.js launch --strict --json` | 1 | BLOCKED (`mcp.up` failed at `mcp-filesystem`) | + +### Key observations + +- `env plan` confirms the expected default launch gate profiles and `mcp-filesystem` read-only default mode. +- `env docker-login` now succeeds in the captured rerun (no non-TTY blocker in this run context). +- `env up` fails deterministically at `mcp-filesystem` with `failed to resolve reference ... : not found` for: + - `ghcr.io/context-labs/filesystem-mcp:latest` +- `env codex-register --replace` succeeds and `codex mcp list --json` includes: + - `filesystem-local` + - `playwright-local` + - `desktop-commander` +- `doctor --strict --json` remains valid JSON and reports: + - `ok=false` + - `summary: passed=4 failed=3 total=7` + - failed checks are the 3 default MCP profiles only +- `launch --strict --json` returns valid JSON on stdout (fixed output contract) and reports: + - `ok=false` + - `nextAction="stop"` + - failing step `mcp.up` + - failed profile `mcp-filesystem` + +## Post-Login GHCR Pull Verification (Direct Docker Pulls) + +Additional evidence captured in the rerun folder (not part of the 1-13 runbook steps) confirms the same blocker for all default MCP images: + +- `docker pull ghcr.io/context-labs/filesystem-mcp:latest` + - Artifact: `/Users/chrisdukes/LocalProjects/codex-synaptic/docs/uat/evidence/2026-02-24-rerun-1/00a-ghcr-pull-filesystem.out.txt` + - Result: `not found` +- `docker pull ghcr.io/context-labs/playwright-mcp:latest` + - Artifact: `/Users/chrisdukes/LocalProjects/codex-synaptic/docs/uat/evidence/2026-02-24-rerun-1/00b-ghcr-pull-playwright.out.txt` + - Result: `not found` +- `docker pull ghcr.io/wonderwhy-er/desktop-commander:latest` + - Artifact: `/Users/chrisdukes/LocalProjects/codex-synaptic/docs/uat/evidence/2026-02-24-rerun-1/00c-ghcr-pull-desktop-commander.out.txt` + - Result: `not found` + +Interpretation: + +- The host is no longer failing with the earlier `denied` auth response. +- Current blocker is image reference availability/path/tag/access-masking as `not found`. + +## Launch JSON Output Contract (Resolved) + +Previous blocked run (`/docs/uat/evidence/2026-02-24/`) discovered stdout contamination in `launch --strict --json`. + +Current rerun status: + +- `/Users/chrisdukes/LocalProjects/codex-synaptic/docs/uat/evidence/2026-02-24-rerun-1/launch.strict.json` is directly parseable JSON. +- No helper extraction artifacts (`launch.strict.payload.json`, stdout prefix stripping) were required for the rerun. + +## Blocker Classification + +### Environment blockers (current) + +- Default MCP image references used by launch-gate profiles are not pullable (`not found`) on this UAT host: + - `ghcr.io/context-labs/filesystem-mcp:latest` + - `ghcr.io/context-labs/playwright-mcp:latest` + - `ghcr.io/wonderwhy-er/desktop-commander:latest` + +### Code regressions (current) + +- None blocking UAT in the rerun. +- `launch --json` stdout purity issue is fixed and re-verified. + +## Secret Handling Note + +- During rerun evidence review, `codex mcp list --json` again contained one secret-like value in a `bearer_token_env_var` field for an unrelated MCP entry. +- The value was redacted in place in: + - `/Users/chrisdukes/LocalProjects/codex-synaptic/docs/uat/evidence/2026-02-24-rerun-1/codex.mcp.list.json` +- Redaction note: + - `/Users/chrisdukes/LocalProjects/codex-synaptic/docs/uat/evidence/2026-02-24-rerun-1/11-codex-mcp-list.redaction.txt` +- No secret values are reproduced in this report. + +## Residual Non-UAT Risk (Previously Triaged, Unchanged) + +- Production `npm audit` still reports deferred high-severity findings in the `sqlite3` install toolchain path (`node-gyp`/`tar`/related transitive dependencies). +- This remains an install-time hardening concern, not the direct runtime blocker for Codex macOS launch/doctor UAT. + +## Remediation Prerequisites To Unblock Final UAT PASS + +1. Confirm the correct, currently available image references (repository path + tag) for the default MCP profiles: + - `mcp-filesystem` + - `mcp-playwright` + - `mcp-desktop-commander` +2. Update profile image references (and runbook/docs if needed) if upstream paths/tags changed. +3. Re-verify direct pulls for all 3 images on the UAT host. +4. Re-run runbook steps 5-9 (or full steps 1-13) and refresh evidence/report. + +## Recommended Next Action + +- Treat UAT readiness as `BLOCKED` pending correction/restoration of the default MCP image references (or equivalent pullable tags). The launch JSON stdout contract issue is resolved and should no longer block reruns. diff --git a/docs/uat/UAT_READINESS_TRACKER.md b/docs/uat/UAT_READINESS_TRACKER.md new file mode 100644 index 0000000..54d4a98 --- /dev/null +++ b/docs/uat/UAT_READINESS_TRACKER.md @@ -0,0 +1,617 @@ +# UAT Readiness Tracker (Codex for macOS Integration) + +## Scope + +UAT readiness (not PRD) for deterministic, testable Codex for macOS launch/doctor MCP readiness flow. + +Primary objectives (priority order): + +- A. Reproducible `launch --strict --json` and `doctor --strict --json` behavior in a documented UAT environment +- B. UAT runbook/checklist with exact commands and pass/fail criteria +- C. CI coverage for non-MCP gates (build/test/lint/preflight) +- D. Local secret hygiene guardrails (`src/cli/.env` auto-loading path) +- E. Package publication scope hardening +- F. Dependency audit remediation/triage + +## Working Rules + +- Small, safe chunks only (one major subsystem at a time) +- Update this file after each chunk with: changes, verification, risks, next exact step +- Respect `AGENTS.md` / `README.md` launch gate semantics as source of truth +- Do not print secrets (especially local `.env` values) + +## Chunk Plan + +1. Chunk 1: Reproduce and harden MCP/launch diagnostics (no broad refactors) — `DONE` +2. Chunk 2: UAT bootstrap/runbook + acceptance checklist — `DONE` +3. Chunk 3: CI workflow for build/test/lint/preflight — `DONE` +4. Chunk 4: Secret hygiene guardrails for local `.env` loading — `DONE` +5. Chunk 5: Packaging scope hardening (`npm pack` contents) +6. Chunk 6: Dependency audit remediation/triage + risk documentation +7. Chunk 7: Full UAT smoke run + final PASS/FAIL report + +## Chunk Log + +### Chunk 1 — MCP/Launch Diagnostics Hardening + +- Status: `PASS` (diagnostics hardening complete; UAT readiness still blocked by MCP image registry access) +- Goal: Make MCP startup failures in `launch`/`doctor` more deterministic and actionable (especially Docker/GHCR image pull/auth failures) without changing launch gate semantics. +- Notes (pre-change): + - Verified docs/source-of-truth references in `AGENTS.md` and `README.md` for default launch gate MCP profiles. + - Identified current weakness: `launch` collapses MCP startup failures into a generic `ensureService` error path. + +#### What changed + +- `src/env/service-manager.ts` + - `ensureService()` now captures Docker Compose startup failures and wraps them with classified, actionable errors. + - Added classification for common Docker/MCP failure modes, including registry/image pull auth denial (GHCR-style `error from registry: denied`), daemon unavailable, and missing Docker CLI. + - Error messages now include profile name, compose command, exit status, and truncated raw Docker output for deterministic debugging. +- `src/cli/launch.ts` + - MCP startup now tracks the specific failing profile and any previously started profiles. + - `mcp.up` launch step now emits profile-specific failure details and metadata (`failedProfile`, `startedProfiles`). + - `mcp.up` remediation now includes `env status ` in addition to docker-login/up/register. +- `src/cli/doctor.ts` + - Failing MCP profile checks now include `codex-synaptic env docker-login ` before `env up` when the profile depends on registry-hosted images. +- Tests + - Updated launch/doctor tests to assert the new remediation/details behavior. + +#### Verification evidence + +- `npx vitest run tests/cli/launch.test.ts tests/cli/doctor.test.ts tests/env/service-manager.test.ts` + - Passed (`15/15` tests) +- `npx tsc --noEmit` + - Passed (no output / exit 0) +- `npm run build` + - Passed (rebuilt `dist/`) +- `node dist/cli/index.js doctor --strict --skip-codex-auth --json` + - Expected fail (exit 1) due default MCP profiles not running/registered + - Improvement verified: each failing default MCP check now includes `env docker-login ` in remediation +- `node dist/cli/index.js launch --strict --skip-codex-auth --json` + - Expected fail (exit 1) on `mcp.up` + - Improvement verified: failure is now deterministic and profile-specific (`mcp-filesystem`), with classified cause `Docker image pull/auth denied...` and explicit GHCR-focused remediation + - Launch gate semantics preserved: `ok=false`, `nextAction="stop"`, doctor not executed after strict fail-fast + +#### Risks / open questions + +- UAT remains blocked until the UAT environment can authenticate/pull required MCP images (at minimum `ghcr.io/context-labs/filesystem-mcp:latest`; likely also `playwright-mcp` and `desktop-commander`). +- Docker Compose warns that `version` in `docker/mcp/*.yml` is obsolete. This is non-blocking for Chunk 1 but may create noise in UAT evidence. +- `doctor` still does not proactively test registry auth; it now emits better remediation, but actual access is only proven during `env up` / `launch`. + +#### Next exact step + +- Chunk 2: create a UAT bootstrap/runbook + acceptance checklist with exact commands, expected JSON pass/fail fields, and explicit pre-reqs (Docker running, GHCR auth, Codex login, default MCP profile startup/register sequence). + +### Chunk 2 — UAT Bootstrap Runbook + Acceptance Checklist + +- Status: `PASS` +- Goal: Produce a deterministic UAT runbook/checklist for Codex for macOS launch/doctor MCP readiness using repo source-of-truth behavior. + +#### What changed + +- Added `docs/uat/CODEX_MACOS_UAT_RUNBOOK.md` + - Exact bootstrap and readiness commands (`build`, `env plan`, `env docker-login`, `env up`, `env status`, `env codex-register`, `doctor --strict --json`, `launch --strict --json`) + - Explicit pass/fail JSON criteria for `doctor` and `launch` + - Deterministic fail/block conditions for UAT status + - Failure triage paths (registry auth, registration drift, health timeout, Codex auth) + - Operator sign-off acceptance checklist + - Notes on secret handling and `src/cli/.env` banner hygiene + +#### Verification evidence + +- `rg -n "doctor --strict --json|launch --strict --json|env docker-login|mcp-filesystem|nextAction|read-only" docs/uat/CODEX_MACOS_UAT_RUNBOOK.md` + - Passed content spot-check (required commands/criteria present) +- `npx prettier --check docs/uat/CODEX_MACOS_UAT_RUNBOOK.md docs/uat/UAT_READINESS_TRACKER.md` + - Initial check flagged tracker formatting only (no content issue in runbook) +- `npx prettier --write docs/uat/CODEX_MACOS_UAT_RUNBOOK.md docs/uat/UAT_READINESS_TRACKER.md` + - Applied formatting +- `npx prettier --check docs/uat/CODEX_MACOS_UAT_RUNBOOK.md docs/uat/UAT_READINESS_TRACKER.md` + - Passed after formatting + +#### Risks / open questions + +- Runbook uses a fixed example evidence date (`2026-02-23`) in example paths; operators should replace with actual run date. +- UAT remains environment-blocked until GHCR credentials/image pulls succeed (tracked in Chunk 1 risks). +- The runbook intentionally documents current behavior; if CLI JSON schema changes, pass/fail criteria must be updated. + +#### Next exact step + +- Chunk 3: add/strengthen CI for non-MCP gates (`npm run build`, `npm test`, `npm run lint`, `npm run release:preflight`) and document the workflow outcome in this tracker. + +### Chunk 3 — CI Workflow for Non-MCP Gates + +- Status: `PASS` +- Goal: Add a deterministic GitHub Actions workflow for non-MCP gates (`build`, `test`, `lint`, `release:preflight`) and harden it against daemon-state test flakiness. + +#### What changed + +- Added `.github/workflows/ci-non-mcp-gates.yml` + - Triggers: `push`, `pull_request`, `workflow_dispatch` + - Single Ubuntu/Node 20 job with pinned `actions/checkout` and `actions/setup-node` + - Runs `npm ci`, `npm run build`, `npm test`, `npm run lint` + - Runs `npm run release:preflight` only on canonical repo (`clduab11/codex-synaptic`) + - Skips preflight on non-canonical repos (forks) to avoid expected origin-fragment failures + - Sets `CODEX_AUTO_LINK=false` to prevent CLI auto-link side effects in CI + - Isolates `HOME` for the `npm test` step (`${{ runner.temp }}/codex-synaptic-ci-home`) to avoid stale local daemon state causing false failures on self-hosted runners + +#### Verification evidence + +- Workflow syntax/format: + - `npx prettier --check .github/workflows/ci-non-mcp-gates.yml` + - Passed + - `node -e "…js-yaml load…"` + - Passed (`YAML_OK`, later verified `Test` step `HOME` override present) +- Local command verification (mirroring CI gates): + - `CODEX_AUTO_LINK=false npm run build` + - Passed + - `npm run lint` + - Passed with warnings only (0 errors, 5 warnings) + - `npm run release:preflight` + - Expected fail in local dev branch state (dirty working tree) + - Confirmed failure reason is local tracked/untracked changes, not script/runtime breakage +- Test stability verification: + - `npm test` + - Failed locally because an active background daemon in the default state directory changed CLI behavior for daemon-sensitive tests (`commands`, `openai-usage`, `cli-smoke`) + - `HOME="$(mktemp -d)" npm test -- tests/cli/openai-usage.test.ts tests/cli/commands.test.ts tests/e2e/cli-smoke.test.ts` + - Passed (`25/25`), confirming the `HOME` isolation mitigation + - `HOME="$(mktemp -d)" npm test -- --reporter=dot` + - Passed full suite (`245/245`) + +#### Risks / open questions + +- New workflow is validated locally (syntax + command behavior), but not yet executed in GitHub Actions within this branch. +- `release:preflight` is intentionally skipped on non-canonical repositories/forks; this reduces false failures but means forks will not enforce that gate. +- `npm test` emits noisy logs/warnings that are expected in this repo; workflow currently accepts them as long as exit code is `0`. + +#### Next exact step + +- Chunk 4: add secret-hygiene guardrails around local `src/cli/.env` auto-loading (especially banner behavior + safety notes) while preserving backward compatibility. + +### Chunk 4 — Secret Hygiene Guardrails for Local `.env` Auto-Loading + +- Status: `PASS` +- Goal: Preserve `.env` auto-loading compatibility while reducing accidental leakage risk and keeping JSON output deterministic. + +#### What changed + +- Added `src/cli/env-bootstrap.ts` + - Extracted CLI env bootstrap logic into a testable helper module + - Added `CODEX_CLI_ENV_AUTOLOAD=0` support to disable CLI env auto-loading + - Added banner controls: + - `CODEX_CLI_ENV_BANNER=0` to suppress banner + - `CODEX_CLI_ENV_BANNER_VERBOSE=1` to show env source paths + - JSON-mode (`--json`) banner suppression by default (override with `CODEX_CLI_ENV_BANNER_FORCE=1`) + - Default banner is now sanitized (generic local `.env` count, no file paths) and includes a local-sensitive note when `src/cli/.env` is loaded +- Updated `src/cli/index.ts` + - Uses helper module for env bootstrap and banner decisions + - Moves env bootstrap banner output to `stderr` (not `stdout`) to avoid contaminating command output streams +- Added tests in `tests/cli/env-bootstrap.test.ts` + - env parser no-override behavior + - autoload toggle behavior + - JSON-mode banner suppression / forced banner override + - sanitized vs verbose banner formatting + - bootstrap file precedence/order semantics +- Updated UAT runbook notes in `docs/uat/CODEX_MACOS_UAT_RUNBOOK.md` + - documents `CODEX_CLI_ENV_AUTOLOAD=0`, stderr banner behavior, and `CODEX_CLI_ENV_BANNER_VERBOSE=1` caution + +#### Verification evidence + +- `npx vitest run tests/cli/env-bootstrap.test.ts` + - Passed (`5/5`) +- `npx tsc --noEmit` + - Passed +- `npm run build` + - Passed +- `HOME="$(mktemp -d)" npm test -- tests/cli/commands.test.ts tests/e2e/cli-smoke.test.ts tests/cli/openai-usage.test.ts` + - Passed (`25/25`) +- `node -e "...spawnSync doctor --skip-codex-auth --json..."` + - Verified JSON output remains clean (`stdout` starts with `{`) and env banner is suppressed in JSON mode +- `node -e "...spawnSync system status..."` with isolated `HOME` + - Verified env banner appears on `stderr` and not `stdout` + - Verified `stdout` still contains expected command output (`System not started`) + +#### Risks / open questions + +- The new env banner hygiene note is helpful but may increase `stderr` noise for non-JSON commands when `src/cli/.env` is present. +- Existing users who relied on exact env source paths in startup banners will now need `CODEX_CLI_ENV_BANNER_VERBOSE=1`. +- Secret values are still loadable from local env files by design; this chunk reduces leakage risk and output contamination, but does not change trust requirements for local env file management. + +#### Next exact step + +- Chunk 5: tighten package publication scope (`files` whitelist and/or `.npmignore`), verify `npm pack --dry-run` contents, and document residual packaging risks. + +### Chunk 5 — Packaging Scope Hardening (`npm pack` contents) + +- Status: `PASS` +- Goal: Reduce published npm tarball scope to an intentional runtime surface for the CLI without breaking launch/doctor MCP packaging expectations. + +#### What changed + +- Updated `package.json` + - Added a `files` whitelist to explicitly control package contents instead of relying on `.gitignore` fallback behavior. + - Kept compiled/runtime assets and key metadata: + - `dist/` + - `docker/` (required by `env`/`launch`/`doctor` service profiles via `docker/mcp/*.yml` compose paths) + - `config/` (runtime config/strategy/GOAP manifests under repo root) + - `.env.example`, `README.md`, `AGENTS.md`, `CHANGELOG.md`, `LICENSE` + - `docs/codex-synaptic-cheat-codes.md` (used by `cheats sync`) + - `.codex-improvement/SCHEMA_MASTER.yaml` (runtime schema dependency in YAML tooling) + +#### Verification evidence + +- Runtime packaging dependency inspection + - Reviewed `package.json` (`main`, `bin`, scripts) and runtime path usage in `src/env/service-manager.ts` and CLI code. + - Confirmed `env`/`launch`/`doctor` use compose files under `docker/mcp/*.yml`, so `docker/` must remain in package scope. +- Pack contents before hardening: + - `npm pack --dry-run --json` (pre-change) + - Result: `561` entries, `unpackedSize=3131266`, `packageSize=751486` + - Included many non-runtime/dev artifacts (`src/`, `tests/`, `.github/`, `python/`, `refactor/`, hidden local metadata, etc.) +- Pack contents after hardening: + - `CODEX_AUTO_LINK=false npm pack --dry-run --json` + - Result: `285` entries, `unpackedSize=1397812`, `packageSize=319389` + - Package now limited to `dist/`, `docker/`, `config/`, selected docs/metadata, and `package.json` + - Improvement delta: + - entries: `561 -> 285` (reduced by `276`) + - unpacked size: `3131266 -> 1397812` bytes + - tarball size: `751486 -> 319389` bytes +- Build verification (explicit per UAT chunk guidance): + - `CODEX_AUTO_LINK=false npm run build` + - Passed +- CLI smoke sanity: + - `node dist/cli/index.js --help` + - Passed (help output rendered; env banner on stderr is expected behavior from Chunk 4) + +#### Risks / open questions + +- This chunk validates pack scope and local CLI execution, but does **not** run an install-from-tarball smoke test (e.g. `npm pack` + temp install) yet. +- `docs/README.md` is still included alongside `docs/codex-synaptic-cheat-codes.md` due npm file inclusion behavior for the `docs/` subtree; this is acceptable but slightly broader than the single-file intent. +- Further scope reduction is possible (for example narrowing `config/` to only runtime-required subsets), but that increases risk of breaking non-launch CLI features and is not necessary for UAT readiness. + +#### Next exact step + +- Chunk 6: run dependency audit (`npm audit --omit=dev --audit-level=high`), triage/remediate safe upgrades, and document any residual high-severity risk with package/transitive-chain context. + +### Chunk 6 — Dependency Audit Remediation / Triage + +- Status: `PASS` (partial remediation applied; remaining production highs triaged and documented) +- Goal: Reduce/triage high-severity production dependency findings with minimal behavior risk, then rerun impacted gates. + +#### What changed + +- Updated direct dependencies in `package.json` + - `@openai/agents`: `^0.1.10 -> ^0.1.11` (safe patch upgrade) + - `js-yaml`: `^4.1.0 -> ^4.1.1` (safe patch upgrade; resolves direct moderate advisory) +- Updated `package-lock.json` + - Pulled patched transitive chain under `@openai/agents`, including: + - `@modelcontextprotocol/sdk@1.27.0` + - `express@5.2.1` + - `body-parser@2.2.2` + - `qs@6.15.0` + +#### Verification evidence + +- Initial production audit: + - `npm audit --omit=dev --audit-level=high --json` + - Result (before remediation): `11 high`, `3 moderate`, `14 total` + - High findings included: + - `@modelcontextprotocol/sdk` (transitive) + - `qs` / `body-parser` (transitive) + - `sqlite3` + `node-gyp`/`tar` chain + - Moderate finding included direct `js-yaml@4.1.0` +- Dependency path verification (before remediation): + - `npm ls @modelcontextprotocol/sdk @openai/agents qs body-parser js-yaml --all` + - Confirmed chain: + - `@openai/agents@0.1.10 -> @openai/agents-core@0.1.10 -> @modelcontextprotocol/sdk@1.20.1 -> express@5.1.0 -> body-parser@2.2.0 / qs@6.14.0` + - direct `js-yaml@4.1.0` + - `npm ls sqlite3 node-gyp tar cacache make-fetch-happen glob rimraf minimatch --all` + - Confirmed remaining high-severity chain roots under `sqlite3@5.1.7` +- Outdated review (for safe upgrade candidates): + - `npm outdated --json` + - Confirmed patch updates available for `@openai/agents` and `js-yaml`; no obvious newer `sqlite3` target surfaced +- Applied safe upgrades: + - `CODEX_AUTO_LINK=false npm install @openai/agents@0.1.11 js-yaml@4.1.1` + - Passed +- Post-remediation production audit: + - `npm audit --omit=dev --audit-level=high --json` + - Result (after remediation): `9 high`, `0 moderate`, `9 total` + - Cleared findings: + - `@modelcontextprotocol/sdk` high advisories + - `qs` high advisory + - `body-parser` moderate advisory + - direct `js-yaml` moderate advisory +- Post-remediation dependency path verification: + - `npm ls @openai/agents @modelcontextprotocol/sdk express body-parser qs js-yaml --all` + - Verified patched chain: + - `@openai/agents@0.1.11 -> @openai/agents-core@0.1.11 -> @modelcontextprotocol/sdk@1.27.0 -> express@5.2.1 -> body-parser@2.2.2 / qs@6.15.0` + - direct `js-yaml@4.1.1` +- Required post-change gates: + - `CODEX_AUTO_LINK=false npm run build` + - Passed + - `npm run lint` + - Passed with existing warnings only (`0 errors`, `5 warnings`) + - `HOME="$(mktemp -d)" npm test -- --reporter=dot` + - Passed (`41` files, `250` tests) + +#### Residual risk (deferred, documented) + +- Remaining production highs are all tied to the `sqlite3` dependency install toolchain: + - Direct/root: + - `sqlite3@5.1.7` (reported high via `node-gyp` and `tar`) + - Transitive chain under `sqlite3`: + - `node-gyp@8.4.1` + - `tar@6.2.1` + - `make-fetch-happen@9.1.0 -> cacache@15.3.0` + - `glob@7.2.3 -> minimatch@3.1.2` + - `rimraf@3.0.2` + - `@npmcli/move-file@1.1.2` +- Severity: + - `high` (9 remaining findings in production audit), but all within the `sqlite3`/native-install dependency path +- Exploitability/context in this repo: + - Primarily impacts package installation / native rebuild flows (`npm install`, `node-gyp`, tar extraction), not the normal runtime execution path for `launch`, `doctor`, or MCP readiness checks. + - Some `tar` advisories are most relevant when extracting attacker-controlled archives (and at least one is especially relevant on macOS/APFS), which raises operator workstation risk during dependency installation but not during routine CLI command execution after install. +- Why deferred: + - `npm audit` only reports a non-viable auto-fix path via `sqlite3@5.0.2` (`isSemVerMajor=true`, and also a downgrade relative to current `5.1.7`), which is not a safe UAT remediation. + - No straightforward non-breaking `sqlite3` upgrade path is indicated by `npm outdated --json` in the current dependency graph. + - Replacing `sqlite3` or refactoring storage backends is out of scope for this UAT chunk and carries higher regression risk. +- Follow-up recommendation: + - Track upstream `sqlite3`/`node-gyp` remediation availability and re-run audit on each lockfile refresh. + - For PRD hardening, evaluate a migration path away from `sqlite3` (or a maintained fork/path with patched install toolchain) and/or vendor strategy that avoids the vulnerable install chain. + - In CI/UAT environments, keep dependency installation restricted to trusted registries and avoid ad-hoc installs from untrusted sources/archives. + +#### Next exact step + +- Chunk 7: execute the full UAT smoke from `docs/uat/CODEX_MACOS_UAT_RUNBOOK.md`, capture dated evidence artifacts, and produce a final UAT PASS/FAIL/BLOCKED report (likely `BLOCKED` if GHCR auth remains unavailable). + +### Chunk 7 — Full UAT Smoke + Final Report + +- Status: `BLOCKED` (environment blocker confirmed: GHCR image pull/auth); additional `launch --strict --json` stdout contamination issue discovered +- Goal: Execute the runbook smoke path end-to-end, capture evidence under a dated folder, and produce final UAT PASS/FAIL/BLOCKED determination. + +#### What changed + +- Captured UAT smoke evidence under `docs/uat/evidence/2026-02-24/` + - Includes step command/output/exit artifacts, `doctor.strict.json`, `launch.strict.json`, `codex.mcp.list.json`, and status matrix `_status.tsv` +- Added final UAT report: + - `docs/uat/UAT_FINAL_REPORT.md` +- Secret hygiene remediation during evidence capture: + - Redacted 1 secret-like value in `codex.mcp.list.json` (`bearer_token_env_var` field contained a token-looking value for an unrelated MCP entry) + - Original secret value was not copied into tracker/report/chat +- Added helper artifacts (without modifying original launch evidence semantics): + - `launch.strict.payload.json` (JSON payload extracted from first `{`) + - `launch.strict.stdout-prefix.txt` (captured non-JSON stdout prefix line) + +#### Verification evidence + +- Full smoke path executed (runbook-aligned; build used `CODEX_AUTO_LINK=false` to avoid local CLI auto-link side effects) + - `CODEX_AUTO_LINK=false npm run build` → pass + - `codex --help` / `codex mcp --help` / `codex mcp add --help` → pass + - `codex login status` → pass (`Logged in using ChatGPT`) + - `node dist/cli/index.js env plan mcp-filesystem mcp-playwright mcp-desktop-commander` → pass + - verified all 3 default profiles and `mcp-filesystem` read-only default note + - `node dist/cli/index.js env docker-login ...` → fail (non-TTY interactive login) + - observed `error: cannot perform an interactive login from a non-TTY device` + - `node dist/cli/index.js env up ...` → fail (expected environment blocker) + - deterministic classified error for `mcp-filesystem`: + - GHCR image pull/auth denied for `ghcr.io/context-labs/filesystem-mcp:latest` + - `node dist/cli/index.js env status ...` → pass + - all 3 profiles reported `running: no`, `healthy: no` (expected after failed `env up`) + - `node dist/cli/index.js env codex-register ... --replace` → pass + - `codex mcp list --json` → pass + - required names present: `filesystem-local`, `playwright-local`, `desktop-commander` + - `node dist/cli/index.js doctor --strict --json` → fail (expected due MCP profiles down) + - valid JSON output + - summary: `passed=4 failed=3 total=7` + - failed checks were the 3 default MCP profiles only + - `node dist/cli/index.js launch --strict --json` → fail (expected due `mcp.up` blocker) + - payload indicates `ok=false`, `nextAction=\"stop\"`, failed step `mcp.up`, failed profile `mcp-filesystem` + - **new issue:** stdout contained a logger line before JSON, making `launch.strict.json` invalid JSON unless post-processed + +#### Risks / open questions + +- **Primary blocker (environment):** UAT cannot pass until GHCR auth/image pull access is available for default MCP profile images. +- **Secondary blocker/risk (code):** `launch --strict --json` stdout contamination breaks machine-parseable JSON artifacts in this failure path (and may affect success paths if env/service logs continue to emit on stdout). +- Docker Compose warning remains noisy (non-blocking but present in evidence): + - `version` attribute obsolete in `docker/mcp/*.yml` +- Residual dependency audit risk from Chunk 6 remains deferred: + - `sqlite3` / `node-gyp` / `tar` install-toolchain high findings (install-time risk, not normal runtime launch path) + +#### Next exact step + +- Unblock + rerun: + 1. Perform interactive `docker login ghcr.io` (or configure Docker credential helper) with access to required MCP images. + 2. Fix `launch --strict --json` stdout purity (logger output should not precede JSON on stdout). + 3. Re-run Chunk 7 smoke (runbook steps 4-9 minimum, preferably full run) with a fresh dated evidence folder and update `docs/uat/UAT_FINAL_REPORT.md`. + +### Chunk 7 Follow-up A — GHCR Access Recheck (Post Interactive Login) + +- Status: `BLOCKED` (registry/image availability still blocking default MCP startup, but auth posture changed) +- Goal: Re-check required GHCR image pull access after operator completed interactive `docker login ghcr.io`. + +#### What changed + +- Verified post-login behavior for the 3 default MCP image references used by UAT launch profiles. +- Failure mode changed from registry auth `denied` to image reference resolution `not found`, indicating the host is no longer failing at the same unauthenticated registry gate. + +#### Verification evidence + +- `docker pull ghcr.io/context-labs/filesystem-mcp:latest` + - Exit `1`; `failed to resolve reference ... : not found` +- `docker pull ghcr.io/context-labs/playwright-mcp:latest` + - Exit `1`; `failed to resolve reference ... : not found` +- `docker pull ghcr.io/wonderwhy-er/desktop-commander:latest` + - Exit `1`; `failed to resolve reference ... : not found` + +#### Risks / open questions + +- UAT remains environment-blocked because default MCP image references still cannot be pulled. +- Root cause is now likely one of: + - image/tag no longer exists (`latest` tag drift), + - repository path drift, or + - access masking as `not found` for the logged-in principal. +- This is separate from the code-level `launch --strict --json` stdout contamination issue and should be reported independently. + +#### Next exact step + +- Fix `launch --strict --json` stdout purity so JSON artifacts are machine-parseable regardless of environment failure, then run targeted verification and proceed to a fresh Chunk 7 smoke re-run (expected verdict may remain `BLOCKED` if image references stay unresolved). + +### Chunk 7 Follow-up B — `launch --json` Stdout Purity Fix + +- Status: `PASS` (code fix + targeted verification complete; environment blockers still separate) +- Goal: Ensure `launch --json` / `launch --strict --json` emits JSON-only on stdout in failure paths (and preserve pass-path safety via test coverage) without weakening diagnostics. + +#### What changed + +- Updated `src/cli/launch.ts` + - Added an internal launch option (`suppressInfoConsoleLogs`) and a scoped helper that temporarily raises logger console threshold to `WARN` while MCP services are started. + - This suppresses info-level `Logger` console output (for example `Starting service mcp-filesystem`) during JSON launch execution while preserving warnings/errors to stderr and retaining file logging. +- Updated `src/cli/index.ts` + - `launch` command now enables the scoped suppression automatically when `--json` is used. +- Updated `tests/cli/launch.test.ts` + - Added a targeted regression test that simulates an info-level logger emission during MCP startup and asserts no `console.info` leakage when JSON-safe suppression is enabled. + +#### Verification evidence + +- Reproduced pre-fix behavior (before rebuilding `dist`): + - `node -e "...spawnSync('node',['dist/cli/index.js','launch','--strict','--skip-codex-auth','--json'])..."` + - Confirmed `stdoutStartsWithBrace=false` + - First stdout line was the logger prefix (`INFO [env] Starting service mcp-filesystem ...`) +- Required build verification: + - `CODEX_AUTO_LINK=false npm run build` + - Passed +- Targeted tests: + - `HOME="$(mktemp -d)" npm test -- tests/cli/launch.test.ts` + - Passed (`5/5`) +- Runtime verification on rebuilt CLI (expected launch failure due MCP image issue, but stdout JSON must remain clean): + - `node -e "...spawnSync('node',['dist/cli/index.js','launch','--json','--skip-codex-auth'])..."` + - Exit `1` (expected), `stdout` parseable JSON, first stdout line `{` + - `node -e "...spawnSync('node',['dist/cli/index.js','launch','--strict','--json','--skip-codex-auth'])..."` + - Exit `1` (expected), `stdout` parseable JSON, first stdout line `{` + +#### Risks / open questions + +- The fix suppresses only info-level logger console output during MCP startup in JSON launch mode; this is intentionally narrow to avoid broader CLI logging changes. +- If future launch steps emit direct `console.log` output before JSON, additional targeted hardening may be required (current targeted runtime verification did not observe this). + +#### Next exact step + +- Re-run the full Chunk 7 UAT smoke with a fresh dated evidence folder, capture artifacts per runbook, then refresh the final UAT report + tracker verdict using the new GHCR `not found` evidence and the fixed launch JSON output contract. + +### Chunk 7 Follow-up C — Full UAT Smoke Rerun + Report Refresh + +- Status: `BLOCKED` (final blocker is now image reference availability, not launch JSON stdout contamination) +- Goal: Re-run the full Chunk 7 smoke with fresh evidence after GHCR login + launch JSON fix, then refresh the final report/tracker verdict. + +#### What changed + +- Captured a fresh rerun evidence set under `docs/uat/evidence/2026-02-24-rerun-1/` + - Reproduced the full runbook step set (`01-13`) with command/output/exit artifacts and JSON outputs. +- Added explicit post-login GHCR pull verification artifacts in the same evidence folder: + - `00a-ghcr-pull-filesystem.*` + - `00b-ghcr-pull-playwright.*` + - `00c-ghcr-pull-desktop-commander.*` +- Refreshed `docs/uat/UAT_FINAL_REPORT.md` + - Updated verdict remains `BLOCKED` + - `env docker-login` now recorded as `PASS` + - primary blocker updated to image `not found` + - launch JSON stdout contamination marked resolved in rerun evidence +- Secret hygiene: + - Redacted 1 secret-like value in rerun `codex.mcp.list.json` + - Added redaction note file `11-codex-mcp-list.redaction.txt` + +#### Verification evidence + +- Full runbook smoke (rerun): + - `CODEX_AUTO_LINK=false npm run build` → pass + - `codex --help` / `codex mcp --help` / `codex mcp add --help` → pass + - `codex login status` → pass + - `node dist/cli/index.js env plan ...` → pass + - `node dist/cli/index.js env docker-login ...` → pass (`Login Succeeded`, reused existing GHCR credentials) + - `node dist/cli/index.js env up ...` → fail/block (`filesystem-mcp:latest` `not found`) + - `node dist/cli/index.js env status ...` → pass (profiles not running/not healthy after failed startup) + - `node dist/cli/index.js env codex-register ... --replace` → pass + - `codex mcp list --json` → pass (artifact redacted) + - `node dist/cli/index.js doctor --strict --json` → fail/block (3 MCP checks only) + - `node dist/cli/index.js launch --strict --json` → fail/block (`mcp.up` / `mcp-filesystem`) +- Launch JSON output contract (rerun evidence): + - `doctor.strict.json` and `launch.strict.json` both parseable JSON + - `launch.strict.json` no longer requires payload extraction from a prefixed stdout line +- Direct GHCR pull verification (rerun evidence extras): + - all 3 `docker pull` commands return `not found` + +#### Risks / open questions + +- UAT remains blocked until the correct/pullable image references (path/tag) for the default MCP profiles are confirmed and updated (if drifted). +- `env docker-login` is interactive by design but passed in the rerun by reusing existing cached credentials; future automation captures may still need a TTY depending on Docker credential state. +- Docker Compose `version` deprecation warnings remain noisy but non-blocking. + +#### Next exact step + +- Determine the current canonical image references/tags for `mcp-filesystem`, `mcp-playwright`, and `mcp-desktop-commander`; update compose profiles/docs if needed; then rerun Chunk 7 and target final UAT `PASS`. + +### Chunk 7 Follow-up D — Canonical Image Investigation + Playwright Profile Patch + +- Status: `PASS` (investigation complete; safe Playwright migration patched and verified). UAT remains `BLOCKED` by filesystem/desktop commander transport compatibility. +- Goal: Identify current canonical container images/tags for the default MCP profiles and patch compose/runbook where a drop-in migration is safe. + +#### What changed + +- Updated `src/env/service-manager.ts` + - `mcp-playwright` image reference changed from legacy GHCR wrapper to canonical Docker Hub MCP image: + - `ghcr.io/context-labs/playwright-mcp:latest` -> `mcp/playwright:latest` +- Updated `docker/mcp/docker-compose.playwright.yml` + - Image changed to `mcp/playwright:latest` + - Added `--host 0.0.0.0` to the container command so the HTTP/SSE server binds to the container interface (required for compose port publishing) +- Updated `docs/uat/CODEX_MACOS_UAT_RUNBOOK.md` + - Added a dated upstream image migration note documenting current canonical images and the transport compatibility constraint + - Updated Docker-login step guidance to distinguish private registry auth from public Docker Hub `mcp/*` images + - Added `not found` as a first-class image drift/deprecation blocker indicator + - Refreshed `Last updated` date + +#### Investigation findings (source-backed) + +- Legacy wrapper GHCR repos used by this repo are gone (registry `404`, not tag drift): + - `context-labs/filesystem-mcp` + - `context-labs/playwright-mcp` + - `wonderwhy-er/desktop-commander` + - Confirmed via authenticated GHCR registry API (`/v2/.../tags/list`) using local Docker credentials (token acquisition succeeded; repo lookup returned `404`) +- Current canonical images discovered and verified: + - `mcp/playwright:latest` (Docker Hub MCP image; matches official Playwright MCP Docker CLI behavior and supports `--port` / `--host`) + - `mcp/filesystem:latest` (Docker Hub MCP image; stdio-oriented, not HTTP/`--port` compatible) + - `mcp/desktop-commander:latest` (Docker Hub image; stdio-oriented, does not accept `--port`) +- Additional source confirmation: + - `microsoft/playwright-mcp` README documents Docker image usage and `--port`/`--host` + - `modelcontextprotocol/servers` filesystem README documents `mcp/filesystem` Docker usage (stdio command form) + - `wonderwhy-er/DesktopCommanderMCP` README documents `mcp/desktop-commander:latest` Docker usage (stdio command form) + +#### Verification evidence + +- Registry/image existence + capability checks + - `docker pull mcp/playwright:latest` → pass + - `docker run --rm mcp/playwright:latest cli.js --help` → pass; confirms `--port` and `--host` options + - `docker pull mcp/filesystem:latest` → pass + - `docker run --rm mcp/filesystem:latest --port 7040 /projects` → fail as expected (`--port` treated as path arg / stdio-oriented image) + - `docker pull mcp/desktop-commander:latest` → pass + - `docker run --rm mcp/desktop-commander:latest --port 7070` → fail as expected (`node: bad option: --port`) +- GHCR wrapper repo verification (post-login, authenticated GHCR API) + - GHCR token acquisition succeeded for each legacy repo scope + - `GET /v2//tags/list` returned `404` for the legacy wrapper repos (repo no longer present) +- Playwright profile patch verification (rebuilt CLI) + - `CODEX_AUTO_LINK=false npm run build` → pass + - `node dist/cli/index.js env docker-login mcp-playwright` → pass (`No registry authentication required for profiles: mcp-playwright`) + - `node dist/cli/index.js env up mcp-playwright` → pass + - `node dist/cli/index.js env status mcp-playwright` → pass (`running: yes`, `healthy: yes`) + - `docker compose -f docker/mcp/docker-compose.playwright.yml down` → pass (cleanup) + +#### Risks / open questions + +- `mcp-filesystem` and `mcp-desktop-commander` canonical Docker images are not compatible with the current launch-gate architecture because the repo expects HTTP MCP endpoints and these images are stdio-only. +- UAT remains blocked until one of the following is implemented: + - replacement HTTP-capable wrapper images for filesystem + desktop commander, or + - a repo-level redesign of default profile startup/registration to support stdio MCP registration (instead of `codex mcp add --url`) +- `docker/mcp/docker-compose.*.yml` files still emit Compose `version` deprecation warnings (non-blocking noise). + +#### Next exact step + +- Choose the unblock strategy for `mcp-filesystem` and `mcp-desktop-commander`: + 1. find/validate replacement HTTP wrapper images with `--port` support, or + 2. redesign default MCP profile registration/startup to use stdio transport for canonical Docker images; + then rerun Chunk 7 UAT smoke and refresh the final report. diff --git a/docs/uat/evidence/2026-02-24-rerun-1/00a-ghcr-pull-filesystem.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/00a-ghcr-pull-filesystem.cmd.txt new file mode 100644 index 0000000..c03c2b7 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/00a-ghcr-pull-filesystem.cmd.txt @@ -0,0 +1 @@ +docker pull ghcr.io/context-labs/filesystem-mcp:latest diff --git a/docs/uat/evidence/2026-02-24-rerun-1/00a-ghcr-pull-filesystem.exit b/docs/uat/evidence/2026-02-24-rerun-1/00a-ghcr-pull-filesystem.exit new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/00a-ghcr-pull-filesystem.exit @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/00a-ghcr-pull-filesystem.out.txt b/docs/uat/evidence/2026-02-24-rerun-1/00a-ghcr-pull-filesystem.out.txt new file mode 100644 index 0000000..89b2fbd --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/00a-ghcr-pull-filesystem.out.txt @@ -0,0 +1 @@ +Error response from daemon: failed to resolve reference "ghcr.io/context-labs/filesystem-mcp:latest": ghcr.io/context-labs/filesystem-mcp:latest: not found diff --git a/docs/uat/evidence/2026-02-24-rerun-1/00b-ghcr-pull-playwright.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/00b-ghcr-pull-playwright.cmd.txt new file mode 100644 index 0000000..f087227 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/00b-ghcr-pull-playwright.cmd.txt @@ -0,0 +1 @@ +docker pull ghcr.io/context-labs/playwright-mcp:latest diff --git a/docs/uat/evidence/2026-02-24-rerun-1/00b-ghcr-pull-playwright.exit b/docs/uat/evidence/2026-02-24-rerun-1/00b-ghcr-pull-playwright.exit new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/00b-ghcr-pull-playwright.exit @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/00b-ghcr-pull-playwright.out.txt b/docs/uat/evidence/2026-02-24-rerun-1/00b-ghcr-pull-playwright.out.txt new file mode 100644 index 0000000..83467b2 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/00b-ghcr-pull-playwright.out.txt @@ -0,0 +1 @@ +Error response from daemon: failed to resolve reference "ghcr.io/context-labs/playwright-mcp:latest": ghcr.io/context-labs/playwright-mcp:latest: not found diff --git a/docs/uat/evidence/2026-02-24-rerun-1/00c-ghcr-pull-desktop-commander.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/00c-ghcr-pull-desktop-commander.cmd.txt new file mode 100644 index 0000000..349602f --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/00c-ghcr-pull-desktop-commander.cmd.txt @@ -0,0 +1 @@ +docker pull ghcr.io/wonderwhy-er/desktop-commander:latest diff --git a/docs/uat/evidence/2026-02-24-rerun-1/00c-ghcr-pull-desktop-commander.exit b/docs/uat/evidence/2026-02-24-rerun-1/00c-ghcr-pull-desktop-commander.exit new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/00c-ghcr-pull-desktop-commander.exit @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/00c-ghcr-pull-desktop-commander.out.txt b/docs/uat/evidence/2026-02-24-rerun-1/00c-ghcr-pull-desktop-commander.out.txt new file mode 100644 index 0000000..0a03ed0 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/00c-ghcr-pull-desktop-commander.out.txt @@ -0,0 +1 @@ +Error response from daemon: failed to resolve reference "ghcr.io/wonderwhy-er/desktop-commander:latest": ghcr.io/wonderwhy-er/desktop-commander:latest: not found diff --git a/docs/uat/evidence/2026-02-24-rerun-1/01-build.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/01-build.cmd.txt new file mode 100644 index 0000000..bbcd902 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/01-build.cmd.txt @@ -0,0 +1 @@ +CODEX_AUTO_LINK=false npm run build diff --git a/docs/uat/evidence/2026-02-24-rerun-1/01-build.exit b/docs/uat/evidence/2026-02-24-rerun-1/01-build.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/01-build.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/01-build.out.txt b/docs/uat/evidence/2026-02-24-rerun-1/01-build.out.txt new file mode 100644 index 0000000..fa50b91 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/01-build.out.txt @@ -0,0 +1,4 @@ + +> codex-synaptic@1.0.0 build +> tsc && node scripts/dev-cli-link.mjs + diff --git a/docs/uat/evidence/2026-02-24-rerun-1/02-codex-help.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/02-codex-help.cmd.txt new file mode 100644 index 0000000..b279cd0 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/02-codex-help.cmd.txt @@ -0,0 +1 @@ +codex --help diff --git a/docs/uat/evidence/2026-02-24-rerun-1/02-codex-help.exit b/docs/uat/evidence/2026-02-24-rerun-1/02-codex-help.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/02-codex-help.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/02-codex-help.out.txt b/docs/uat/evidence/2026-02-24-rerun-1/02-codex-help.out.txt new file mode 100644 index 0000000..ad7d567 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/02-codex-help.out.txt @@ -0,0 +1,114 @@ +Codex CLI + +If no subcommand is specified, options will be forwarded to the interactive CLI. + +Usage: codex [OPTIONS] [PROMPT] + codex [OPTIONS] [ARGS] + +Commands: + exec Run Codex non-interactively [aliases: e] + review Run a code review non-interactively + login Manage login + logout Remove stored authentication credentials + mcp [experimental] Run Codex as an MCP server and manage MCP servers + mcp-server [experimental] Run the Codex MCP server (stdio transport) + app-server [experimental] Run the app server or related tooling + app Launch the Codex desktop app (downloads the macOS installer if missing) + completion Generate shell completion scripts + sandbox Run commands within a Codex-provided sandbox + debug Debugging tools + apply Apply the latest diff produced by Codex agent as a `git apply` to your local working + tree [aliases: a] + resume Resume a previous interactive session (picker by default; use --last to continue the + most recent) + fork Fork a previous interactive session (picker by default; use --last to fork the most + recent) + cloud [EXPERIMENTAL] Browse tasks from Codex Cloud and apply changes locally + features Inspect feature flags + help Print this message or the help of the given subcommand(s) + +Arguments: + [PROMPT] + Optional user prompt to start the session + +Options: + -c, --config + Override a configuration value that would otherwise be loaded from `~/.codex/config.toml`. + Use a dotted path (`foo.bar.baz`) to override nested values. The `value` portion is parsed + as TOML. If it fails to parse as TOML, the raw string is used as a literal. + + Examples: - `-c model="o3"` - `-c 'sandbox_permissions=["disk-full-read-access"]'` - `-c + shell_environment_policy.inherit=all` + + --enable + Enable a feature (repeatable). Equivalent to `-c features.=true` + + --disable + Disable a feature (repeatable). Equivalent to `-c features.=false` + + -i, --image ... + Optional image(s) to attach to the initial prompt + + -m, --model + Model the agent should use + + --oss + Convenience flag to select the local open source model provider. Equivalent to -c + model_provider=oss; verifies a local LM Studio or Ollama server is running + + --local-provider + Specify which local provider to use (lmstudio or ollama). If not specified with --oss, + will use config default or show selection + + -p, --profile + Configuration profile from config.toml to specify default options + + -s, --sandbox + Select the sandbox policy to use when executing model-generated shell commands + + [possible values: read-only, workspace-write, danger-full-access] + + -a, --ask-for-approval + Configure when the model requires human approval before executing a command + + Possible values: + - untrusted: Only run "trusted" commands (e.g. ls, cat, sed) without asking for user + approval. Will escalate to the user if the model proposes a command that is not in the + "trusted" set + - on-failure: Run all commands without asking for user approval. Only asks for approval if + a command fails to execute, in which case it will escalate to the user to ask for + un-sandboxed execution + - on-request: The model decides when to ask the user for approval + - never: Never ask for user approval Execution failures are immediately returned to + the model + + --full-auto + Convenience alias for low-friction sandboxed automatic execution (-a on-request, --sandbox + workspace-write) + + --dangerously-bypass-approvals-and-sandbox + Skip all confirmation prompts and execute commands without sandboxing. EXTREMELY + DANGEROUS. Intended solely for running in environments that are externally sandboxed + + -C, --cd + Tell the agent to use the specified directory as its working root + + --search + Enable live web search. When enabled, the native Responses `web_search` tool is available + to the model (no per‑call approval) + + --add-dir + Additional directories that should be writable alongside the primary workspace + + --no-alt-screen + Disable alternate screen mode + + Runs the TUI in inline mode, preserving terminal scrollback history. This is useful in + terminal multiplexers like Zellij that follow the xterm spec strictly and disable + scrollback in alternate screen buffers. + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version diff --git a/docs/uat/evidence/2026-02-24-rerun-1/03-codex-mcp-help.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/03-codex-mcp-help.cmd.txt new file mode 100644 index 0000000..3eeee4f --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/03-codex-mcp-help.cmd.txt @@ -0,0 +1 @@ +codex mcp --help diff --git a/docs/uat/evidence/2026-02-24-rerun-1/03-codex-mcp-help.exit b/docs/uat/evidence/2026-02-24-rerun-1/03-codex-mcp-help.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/03-codex-mcp-help.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/03-codex-mcp-help.out.txt b/docs/uat/evidence/2026-02-24-rerun-1/03-codex-mcp-help.out.txt new file mode 100644 index 0000000..10b3886 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/03-codex-mcp-help.out.txt @@ -0,0 +1,30 @@ +[experimental] Run Codex as an MCP server and manage MCP servers + +Usage: codex mcp [OPTIONS] + +Commands: + list + get + add + remove + login + logout + help Print this message or the help of the given subcommand(s) + +Options: + -c, --config + Override a configuration value that would otherwise be loaded from `~/.codex/config.toml`. + Use a dotted path (`foo.bar.baz`) to override nested values. The `value` portion is parsed + as TOML. If it fails to parse as TOML, the raw string is used as a literal. + + Examples: - `-c model="o3"` - `-c 'sandbox_permissions=["disk-full-read-access"]'` - `-c + shell_environment_policy.inherit=all` + + --enable + Enable a feature (repeatable). Equivalent to `-c features.=true` + + --disable + Disable a feature (repeatable). Equivalent to `-c features.=false` + + -h, --help + Print help (see a summary with '-h') diff --git a/docs/uat/evidence/2026-02-24-rerun-1/04-codex-mcp-add-help.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/04-codex-mcp-add-help.cmd.txt new file mode 100644 index 0000000..785dcc7 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/04-codex-mcp-add-help.cmd.txt @@ -0,0 +1 @@ +codex mcp add --help diff --git a/docs/uat/evidence/2026-02-24-rerun-1/04-codex-mcp-add-help.exit b/docs/uat/evidence/2026-02-24-rerun-1/04-codex-mcp-add-help.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/04-codex-mcp-add-help.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/04-codex-mcp-add-help.out.txt b/docs/uat/evidence/2026-02-24-rerun-1/04-codex-mcp-add-help.out.txt new file mode 100644 index 0000000..0e8cb52 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/04-codex-mcp-add-help.out.txt @@ -0,0 +1,36 @@ +Usage: codex mcp add [OPTIONS] (--url | -- ...) + +Arguments: + + Name for the MCP server configuration + + [COMMAND]... + Command to launch the MCP server. Use --url for a streamable HTTP server + +Options: + -c, --config + Override a configuration value that would otherwise be loaded from `~/.codex/config.toml`. + Use a dotted path (`foo.bar.baz`) to override nested values. The `value` portion is parsed + as TOML. If it fails to parse as TOML, the raw string is used as a literal. + + Examples: - `-c model="o3"` - `-c 'sandbox_permissions=["disk-full-read-access"]'` - `-c + shell_environment_policy.inherit=all` + + --env + Environment variables to set when launching the server. Only valid with stdio servers + + --enable + Enable a feature (repeatable). Equivalent to `-c features.=true` + + --url + URL for a streamable HTTP MCP server + + --bearer-token-env-var + Optional environment variable to read for a bearer token. Only valid with streamable HTTP + servers + + --disable + Disable a feature (repeatable). Equivalent to `-c features.=false` + + -h, --help + Print help (see a summary with '-h') diff --git a/docs/uat/evidence/2026-02-24-rerun-1/05-codex-login-status.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/05-codex-login-status.cmd.txt new file mode 100644 index 0000000..b463ad9 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/05-codex-login-status.cmd.txt @@ -0,0 +1 @@ +codex login status diff --git a/docs/uat/evidence/2026-02-24-rerun-1/05-codex-login-status.exit b/docs/uat/evidence/2026-02-24-rerun-1/05-codex-login-status.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/05-codex-login-status.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/05-codex-login-status.out.txt b/docs/uat/evidence/2026-02-24-rerun-1/05-codex-login-status.out.txt new file mode 100644 index 0000000..39390a3 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/05-codex-login-status.out.txt @@ -0,0 +1 @@ +Logged in using ChatGPT diff --git a/docs/uat/evidence/2026-02-24-rerun-1/06-env-plan.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/06-env-plan.cmd.txt new file mode 100644 index 0000000..2d3ed05 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/06-env-plan.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js env plan mcp-filesystem mcp-playwright mcp-desktop-commander diff --git a/docs/uat/evidence/2026-02-24-rerun-1/06-env-plan.exit b/docs/uat/evidence/2026-02-24-rerun-1/06-env-plan.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/06-env-plan.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/06-env-plan.out.txt b/docs/uat/evidence/2026-02-24-rerun-1/06-env-plan.out.txt new file mode 100644 index 0000000..834c222 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/06-env-plan.out.txt @@ -0,0 +1,24 @@ +⚙️ Environment variables loaded from local .env file(s) (1). +🔒 `src/cli/.env` is local-sensitive state. Avoid sharing logs/screenshots with local env details; set CODEX_CLI_ENV_AUTOLOAD=0 to disable auto-loading. + +mcp-filesystem + description: Local filesystem MCP server (read-only by default) + compose: docker/mcp/docker-compose.filesystem.yml + services: mcp-filesystem + port: 7040 + codex mcp name: filesystem-local + filesystem mode: read-only (default) or controlled-write with explicit approval + +mcp-playwright + description: Playwright automation MCP server + compose: docker/mcp/docker-compose.playwright.yml + services: mcp-playwright + port: 7030 + codex mcp name: playwright-local + +mcp-desktop-commander + description: Desktop Commander MCP server for desktop/tooling automation + compose: docker/mcp/docker-compose.desktop-commander.yml + services: mcp-desktop-commander + port: 7070 + codex mcp name: desktop-commander diff --git a/docs/uat/evidence/2026-02-24-rerun-1/07-env-docker-login.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/07-env-docker-login.cmd.txt new file mode 100644 index 0000000..e589dd2 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/07-env-docker-login.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js env docker-login mcp-filesystem mcp-playwright mcp-desktop-commander diff --git a/docs/uat/evidence/2026-02-24-rerun-1/07-env-docker-login.exit b/docs/uat/evidence/2026-02-24-rerun-1/07-env-docker-login.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/07-env-docker-login.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/07-env-docker-login.out.txt b/docs/uat/evidence/2026-02-24-rerun-1/07-env-docker-login.out.txt new file mode 100644 index 0000000..c35081f --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/07-env-docker-login.out.txt @@ -0,0 +1,10 @@ +⚙️ Environment variables loaded from local .env file(s) (1). +🔒 `src/cli/.env` is local-sensitive state. Avoid sharing logs/screenshots with local env details; set CODEX_CLI_ENV_AUTOLOAD=0 to disable auto-loading. +2026-02-24T15:02:53.043Z ✨ INFO [env] Authenticating Docker registry ~ all green—smooth sailing ✨ | data: registry=ghcr.io +Authenticating with existing credentials... [Username: clduab11] + + Info -> To login with a different account, run 'docker logout' followed by 'docker login' + + +Login Succeeded +✅ Docker auth completed for ghcr.io diff --git a/docs/uat/evidence/2026-02-24-rerun-1/08-env-up.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/08-env-up.cmd.txt new file mode 100644 index 0000000..39e36c3 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/08-env-up.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js env up mcp-filesystem mcp-playwright mcp-desktop-commander diff --git a/docs/uat/evidence/2026-02-24-rerun-1/08-env-up.exit b/docs/uat/evidence/2026-02-24-rerun-1/08-env-up.exit new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/08-env-up.exit @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/08-env-up.out.txt b/docs/uat/evidence/2026-02-24-rerun-1/08-env-up.out.txt new file mode 100644 index 0000000..811a79b --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/08-env-up.out.txt @@ -0,0 +1,7 @@ +⚙️ Environment variables loaded from local .env file(s) (1). +🔒 `src/cli/.env` is local-sensitive state. Avoid sharing logs/screenshots with local env details; set CODEX_CLI_ENV_AUTOLOAD=0 to disable auto-loading. +2026-02-24T15:02:58.761Z ✨ INFO [env] Starting service mcp-filesystem ~ all green—smooth sailing ✨ | data: command=docker compose -f docker/mcp/docker-c… +❌ env.up failed: Docker compose startup failed for mcp-filesystem (exit=1, compose=docker compose -f docker/mcp/docker-compose.filesystem.yml up -d mcp-filesystem). Verify Docker is running, then retry. Raw docker output: time="2026-02-24T09:02:58-06:00" level=warning msg="/Users/chrisdukes/LocalProjects/codex-synaptic/docker/mcp/docker-compose.filesystem.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion" + Image ghcr.io/context-labs/filesystem-mcp:latest Pulling + Image ghcr.io/context-labs/filesystem-mcp:latest Error failed to resolve reference "ghcr.io/context-labs/filesystem-mcp:latest": ghcr.io/context-labs/filesystem-mcp:latest: not found +Error respon… diff --git a/docs/uat/evidence/2026-02-24-rerun-1/09-env-status.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/09-env-status.cmd.txt new file mode 100644 index 0000000..86076b2 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/09-env-status.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js env status mcp-filesystem mcp-playwright mcp-desktop-commander diff --git a/docs/uat/evidence/2026-02-24-rerun-1/09-env-status.exit b/docs/uat/evidence/2026-02-24-rerun-1/09-env-status.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/09-env-status.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/09-env-status.out.txt b/docs/uat/evidence/2026-02-24-rerun-1/09-env-status.out.txt new file mode 100644 index 0000000..a477052 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/09-env-status.out.txt @@ -0,0 +1,20 @@ +⚙️ Environment variables loaded from local .env file(s) (1). +🔒 `src/cli/.env` is local-sensitive state. Avoid sharing logs/screenshots with local env details; set CODEX_CLI_ENV_AUTOLOAD=0 to disable auto-loading. + +mcp-filesystem + running: no + healthy: no + checkedAt: 2026-02-24T15:03:00.248Z +NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS + +mcp-playwright + running: no + healthy: no + checkedAt: 2026-02-24T15:03:00.325Z +NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS + +mcp-desktop-commander + running: no + healthy: no + checkedAt: 2026-02-24T15:03:00.400Z +NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS diff --git a/docs/uat/evidence/2026-02-24-rerun-1/10-env-codex-register.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/10-env-codex-register.cmd.txt new file mode 100644 index 0000000..cd3eac6 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/10-env-codex-register.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js env codex-register mcp-filesystem mcp-playwright mcp-desktop-commander --replace diff --git a/docs/uat/evidence/2026-02-24-rerun-1/10-env-codex-register.exit b/docs/uat/evidence/2026-02-24-rerun-1/10-env-codex-register.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/10-env-codex-register.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/10-env-codex-register.out.txt b/docs/uat/evidence/2026-02-24-rerun-1/10-env-codex-register.out.txt new file mode 100644 index 0000000..6819610 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/10-env-codex-register.out.txt @@ -0,0 +1,8 @@ +⚙️ Environment variables loaded from local .env file(s) (1). +🔒 `src/cli/.env` is local-sensitive state. Avoid sharing logs/screenshots with local env details; set CODEX_CLI_ENV_AUTOLOAD=0 to disable auto-loading. +Removed existing Codex MCP entry: filesystem-local +✅ Registered Codex MCP server filesystem-local -> http://localhost:7040 +Removed existing Codex MCP entry: playwright-local +✅ Registered Codex MCP server playwright-local -> http://localhost:7030 +Removed existing Codex MCP entry: desktop-commander +✅ Registered Codex MCP server desktop-commander -> http://localhost:7070 diff --git a/docs/uat/evidence/2026-02-24-rerun-1/11-codex-mcp-list.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/11-codex-mcp-list.cmd.txt new file mode 100644 index 0000000..fbbaaa8 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/11-codex-mcp-list.cmd.txt @@ -0,0 +1 @@ +codex mcp list --json diff --git a/docs/uat/evidence/2026-02-24-rerun-1/11-codex-mcp-list.err.txt b/docs/uat/evidence/2026-02-24-rerun-1/11-codex-mcp-list.err.txt new file mode 100644 index 0000000..e69de29 diff --git a/docs/uat/evidence/2026-02-24-rerun-1/11-codex-mcp-list.exit b/docs/uat/evidence/2026-02-24-rerun-1/11-codex-mcp-list.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/11-codex-mcp-list.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/11-codex-mcp-list.redaction.txt b/docs/uat/evidence/2026-02-24-rerun-1/11-codex-mcp-list.redaction.txt new file mode 100644 index 0000000..a137539 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/11-codex-mcp-list.redaction.txt @@ -0,0 +1 @@ +Redacted 1 secret-like value in codex.mcp.list.json (bearer_token_env_var field) on 2026-02-24 rerun capture. diff --git a/docs/uat/evidence/2026-02-24-rerun-1/12-doctor-strict-json.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/12-doctor-strict-json.cmd.txt new file mode 100644 index 0000000..01187f9 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/12-doctor-strict-json.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js doctor --strict --json diff --git a/docs/uat/evidence/2026-02-24-rerun-1/12-doctor-strict-json.err.txt b/docs/uat/evidence/2026-02-24-rerun-1/12-doctor-strict-json.err.txt new file mode 100644 index 0000000..a258054 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/12-doctor-strict-json.err.txt @@ -0,0 +1 @@ +❌ doctor failed: Doctor found 3 failing check(s). diff --git a/docs/uat/evidence/2026-02-24-rerun-1/12-doctor-strict-json.exit b/docs/uat/evidence/2026-02-24-rerun-1/12-doctor-strict-json.exit new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/12-doctor-strict-json.exit @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/13-launch-strict-json.cmd.txt b/docs/uat/evidence/2026-02-24-rerun-1/13-launch-strict-json.cmd.txt new file mode 100644 index 0000000..c9e5c45 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/13-launch-strict-json.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js launch --strict --json diff --git a/docs/uat/evidence/2026-02-24-rerun-1/13-launch-strict-json.err.txt b/docs/uat/evidence/2026-02-24-rerun-1/13-launch-strict-json.err.txt new file mode 100644 index 0000000..eda2d11 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/13-launch-strict-json.err.txt @@ -0,0 +1 @@ +❌ launch failed: Launch failed one or more readiness gates. diff --git a/docs/uat/evidence/2026-02-24-rerun-1/13-launch-strict-json.exit b/docs/uat/evidence/2026-02-24-rerun-1/13-launch-strict-json.exit new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/13-launch-strict-json.exit @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24-rerun-1/_status.tsv b/docs/uat/evidence/2026-02-24-rerun-1/_status.tsv new file mode 100644 index 0000000..eadcbbf --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/_status.tsv @@ -0,0 +1,17 @@ +step exit artifact +01-build 0 01-build.out.txt +02-codex-help 0 02-codex-help.out.txt +03-codex-mcp-help 0 03-codex-mcp-help.out.txt +04-codex-mcp-add-help 0 04-codex-mcp-add-help.out.txt +05-codex-login-status 0 05-codex-login-status.out.txt +06-env-plan 0 06-env-plan.out.txt +07-env-docker-login 0 07-env-docker-login.out.txt +08-env-up 1 08-env-up.out.txt +09-env-status 0 09-env-status.out.txt +10-env-codex-register 0 10-env-codex-register.out.txt +11-codex-mcp-list 0 codex.mcp.list.json +12-doctor-strict-json 1 doctor.strict.json +13-launch-strict-json 1 launch.strict.json +00a-ghcr-pull-filesystem 1 00a-ghcr-pull-filesystem.out.txt +00b-ghcr-pull-playwright 1 00b-ghcr-pull-playwright.out.txt +00c-ghcr-pull-desktop-commander 1 00c-ghcr-pull-desktop-commander.out.txt diff --git a/docs/uat/evidence/2026-02-24-rerun-1/codex.mcp.list.json b/docs/uat/evidence/2026-02-24-rerun-1/codex.mcp.list.json new file mode 100644 index 0000000..c35fe63 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/codex.mcp.list.json @@ -0,0 +1,125 @@ +[ + { + "name": "desktop-commander", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "http://localhost:7070", + "bearer_token_env_var": null, + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "unsupported" + }, + { + "name": "figma", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "https://mcp.figma.com/mcp", + "bearer_token_env_var": null, + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "o_auth" + }, + { + "name": "filesystem-local", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "http://localhost:7040", + "bearer_token_env_var": null, + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "unsupported" + }, + { + "name": "linear", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "https://mcp.linear.app/mcp", + "bearer_token_env_var": "[REDACTED_SECRET_LIKE_VALUE]", + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "bearer_token" + }, + { + "name": "notion", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "https://mcp.notion.com/mcp", + "bearer_token_env_var": null, + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "o_auth" + }, + { + "name": "openaiDeveloperDocs", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "https://developers.openai.com/mcp", + "bearer_token_env_var": null, + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "unsupported" + }, + { + "name": "playwright", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "stdio", + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ], + "env": null, + "env_vars": [], + "cwd": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "unsupported" + }, + { + "name": "playwright-local", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "http://localhost:7030", + "bearer_token_env_var": null, + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "unsupported" + } +] diff --git a/docs/uat/evidence/2026-02-24-rerun-1/doctor.strict.json b/docs/uat/evidence/2026-02-24-rerun-1/doctor.strict.json new file mode 100644 index 0000000..b39c38d --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/doctor.strict.json @@ -0,0 +1,60 @@ +{ + "ok": false, + "summary": { + "passed": 4, + "failed": 3, + "total": 7 + }, + "checks": [ + { + "id": "repo.cli_build_artifact", + "ok": true, + "details": "Found /Users/chrisdukes/LocalProjects/codex-synaptic/dist/cli/index.js" + }, + { + "id": "repo.cli_exec", + "ok": true, + "details": "CLI help command succeeded." + }, + { + "id": "codex.auth", + "ok": true, + "details": "Logged in using ChatGPT" + }, + { + "id": "codex.mcp_list", + "ok": true, + "details": "Loaded 8 Codex MCP registration(s)." + }, + { + "id": "mcp.mcp-filesystem", + "ok": false, + "details": "running=false healthy=false registered=true", + "remediation": "codex-synaptic env docker-login mcp-filesystem && codex-synaptic env up mcp-filesystem", + "metadata": { + "codexName": "filesystem-local", + "url": "http://localhost:7040" + } + }, + { + "id": "mcp.mcp-playwright", + "ok": false, + "details": "running=false healthy=false registered=true", + "remediation": "codex-synaptic env docker-login mcp-playwright && codex-synaptic env up mcp-playwright", + "metadata": { + "codexName": "playwright-local", + "url": "http://localhost:7030" + } + }, + { + "id": "mcp.mcp-desktop-commander", + "ok": false, + "details": "running=false healthy=false registered=true", + "remediation": "codex-synaptic env docker-login mcp-desktop-commander && codex-synaptic env up mcp-desktop-commander", + "metadata": { + "codexName": "desktop-commander", + "url": "http://localhost:7070" + } + } + ] +} diff --git a/docs/uat/evidence/2026-02-24-rerun-1/launch.strict.json b/docs/uat/evidence/2026-02-24-rerun-1/launch.strict.json new file mode 100644 index 0000000..6f8ed92 --- /dev/null +++ b/docs/uat/evidence/2026-02-24-rerun-1/launch.strict.json @@ -0,0 +1,40 @@ +{ + "ok": false, + "steps": [ + { + "id": "repo.preflight", + "ok": true, + "details": "Found /Users/chrisdukes/LocalProjects/codex-synaptic/dist/cli/index.js; CLI executable check passed." + }, + { + "id": "codex.auth", + "ok": true, + "details": "Logged in using ChatGPT" + }, + { + "id": "runtime.daemon", + "ok": true, + "details": "Background daemon already running (pid 64145)." + }, + { + "id": "mcp.up", + "ok": false, + "details": "Failed to start MCP profile mcp-filesystem after starting 0/3: Docker compose startup failed for mcp-filesystem (exit=1, compose=docker compose -f docker/mcp/docker-compose.filesystem.yml up -d mcp-filesystem). Verify Docker is running, then retry. Raw docker output: time=\"2026-02-24T09:03:04-06:00\" level=warning msg=\"/Users/chrisdukes/LocalProjects/codex-synaptic/docker/mcp/docker-compose.filesystem.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion\"\n Image ghcr.io/context-labs/filesystem-mcp:latest Pulling \n Image ghcr.io/context-labs/filesystem-mcp:latest Error failed to resolve reference \"ghcr.io/context-labs/filesystem-mcp:latest\": ghcr.io/context-labs/filesystem-mcp:latest: not found\nError respon…", + "remediation": "codex-synaptic env docker-login mcp-filesystem && codex-synaptic env up mcp-filesystem && codex-synaptic env codex-register mcp-filesystem --replace && codex-synaptic env status mcp-filesystem", + "metadata": { + "failedProfile": "mcp-filesystem", + "startedProfiles": [] + } + } + ], + "doctor": { + "ok": false, + "summary": { + "passed": 0, + "failed": 0, + "total": 0 + }, + "checks": [] + }, + "nextAction": "stop" +} diff --git a/docs/uat/evidence/2026-02-24/01-build.cmd.txt b/docs/uat/evidence/2026-02-24/01-build.cmd.txt new file mode 100644 index 0000000..bbcd902 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/01-build.cmd.txt @@ -0,0 +1 @@ +CODEX_AUTO_LINK=false npm run build diff --git a/docs/uat/evidence/2026-02-24/01-build.exit b/docs/uat/evidence/2026-02-24/01-build.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/01-build.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24/01-build.out.txt b/docs/uat/evidence/2026-02-24/01-build.out.txt new file mode 100644 index 0000000..fa50b91 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/01-build.out.txt @@ -0,0 +1,4 @@ + +> codex-synaptic@1.0.0 build +> tsc && node scripts/dev-cli-link.mjs + diff --git a/docs/uat/evidence/2026-02-24/02-codex-help.cmd.txt b/docs/uat/evidence/2026-02-24/02-codex-help.cmd.txt new file mode 100644 index 0000000..b279cd0 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/02-codex-help.cmd.txt @@ -0,0 +1 @@ +codex --help diff --git a/docs/uat/evidence/2026-02-24/02-codex-help.exit b/docs/uat/evidence/2026-02-24/02-codex-help.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/02-codex-help.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24/02-codex-help.out.txt b/docs/uat/evidence/2026-02-24/02-codex-help.out.txt new file mode 100644 index 0000000..ad7d567 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/02-codex-help.out.txt @@ -0,0 +1,114 @@ +Codex CLI + +If no subcommand is specified, options will be forwarded to the interactive CLI. + +Usage: codex [OPTIONS] [PROMPT] + codex [OPTIONS] [ARGS] + +Commands: + exec Run Codex non-interactively [aliases: e] + review Run a code review non-interactively + login Manage login + logout Remove stored authentication credentials + mcp [experimental] Run Codex as an MCP server and manage MCP servers + mcp-server [experimental] Run the Codex MCP server (stdio transport) + app-server [experimental] Run the app server or related tooling + app Launch the Codex desktop app (downloads the macOS installer if missing) + completion Generate shell completion scripts + sandbox Run commands within a Codex-provided sandbox + debug Debugging tools + apply Apply the latest diff produced by Codex agent as a `git apply` to your local working + tree [aliases: a] + resume Resume a previous interactive session (picker by default; use --last to continue the + most recent) + fork Fork a previous interactive session (picker by default; use --last to fork the most + recent) + cloud [EXPERIMENTAL] Browse tasks from Codex Cloud and apply changes locally + features Inspect feature flags + help Print this message or the help of the given subcommand(s) + +Arguments: + [PROMPT] + Optional user prompt to start the session + +Options: + -c, --config + Override a configuration value that would otherwise be loaded from `~/.codex/config.toml`. + Use a dotted path (`foo.bar.baz`) to override nested values. The `value` portion is parsed + as TOML. If it fails to parse as TOML, the raw string is used as a literal. + + Examples: - `-c model="o3"` - `-c 'sandbox_permissions=["disk-full-read-access"]'` - `-c + shell_environment_policy.inherit=all` + + --enable + Enable a feature (repeatable). Equivalent to `-c features.=true` + + --disable + Disable a feature (repeatable). Equivalent to `-c features.=false` + + -i, --image ... + Optional image(s) to attach to the initial prompt + + -m, --model + Model the agent should use + + --oss + Convenience flag to select the local open source model provider. Equivalent to -c + model_provider=oss; verifies a local LM Studio or Ollama server is running + + --local-provider + Specify which local provider to use (lmstudio or ollama). If not specified with --oss, + will use config default or show selection + + -p, --profile + Configuration profile from config.toml to specify default options + + -s, --sandbox + Select the sandbox policy to use when executing model-generated shell commands + + [possible values: read-only, workspace-write, danger-full-access] + + -a, --ask-for-approval + Configure when the model requires human approval before executing a command + + Possible values: + - untrusted: Only run "trusted" commands (e.g. ls, cat, sed) without asking for user + approval. Will escalate to the user if the model proposes a command that is not in the + "trusted" set + - on-failure: Run all commands without asking for user approval. Only asks for approval if + a command fails to execute, in which case it will escalate to the user to ask for + un-sandboxed execution + - on-request: The model decides when to ask the user for approval + - never: Never ask for user approval Execution failures are immediately returned to + the model + + --full-auto + Convenience alias for low-friction sandboxed automatic execution (-a on-request, --sandbox + workspace-write) + + --dangerously-bypass-approvals-and-sandbox + Skip all confirmation prompts and execute commands without sandboxing. EXTREMELY + DANGEROUS. Intended solely for running in environments that are externally sandboxed + + -C, --cd + Tell the agent to use the specified directory as its working root + + --search + Enable live web search. When enabled, the native Responses `web_search` tool is available + to the model (no per‑call approval) + + --add-dir + Additional directories that should be writable alongside the primary workspace + + --no-alt-screen + Disable alternate screen mode + + Runs the TUI in inline mode, preserving terminal scrollback history. This is useful in + terminal multiplexers like Zellij that follow the xterm spec strictly and disable + scrollback in alternate screen buffers. + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version diff --git a/docs/uat/evidence/2026-02-24/03-codex-mcp-help.cmd.txt b/docs/uat/evidence/2026-02-24/03-codex-mcp-help.cmd.txt new file mode 100644 index 0000000..3eeee4f --- /dev/null +++ b/docs/uat/evidence/2026-02-24/03-codex-mcp-help.cmd.txt @@ -0,0 +1 @@ +codex mcp --help diff --git a/docs/uat/evidence/2026-02-24/03-codex-mcp-help.exit b/docs/uat/evidence/2026-02-24/03-codex-mcp-help.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/03-codex-mcp-help.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24/03-codex-mcp-help.out.txt b/docs/uat/evidence/2026-02-24/03-codex-mcp-help.out.txt new file mode 100644 index 0000000..10b3886 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/03-codex-mcp-help.out.txt @@ -0,0 +1,30 @@ +[experimental] Run Codex as an MCP server and manage MCP servers + +Usage: codex mcp [OPTIONS] + +Commands: + list + get + add + remove + login + logout + help Print this message or the help of the given subcommand(s) + +Options: + -c, --config + Override a configuration value that would otherwise be loaded from `~/.codex/config.toml`. + Use a dotted path (`foo.bar.baz`) to override nested values. The `value` portion is parsed + as TOML. If it fails to parse as TOML, the raw string is used as a literal. + + Examples: - `-c model="o3"` - `-c 'sandbox_permissions=["disk-full-read-access"]'` - `-c + shell_environment_policy.inherit=all` + + --enable + Enable a feature (repeatable). Equivalent to `-c features.=true` + + --disable + Disable a feature (repeatable). Equivalent to `-c features.=false` + + -h, --help + Print help (see a summary with '-h') diff --git a/docs/uat/evidence/2026-02-24/04-codex-mcp-add-help.cmd.txt b/docs/uat/evidence/2026-02-24/04-codex-mcp-add-help.cmd.txt new file mode 100644 index 0000000..785dcc7 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/04-codex-mcp-add-help.cmd.txt @@ -0,0 +1 @@ +codex mcp add --help diff --git a/docs/uat/evidence/2026-02-24/04-codex-mcp-add-help.exit b/docs/uat/evidence/2026-02-24/04-codex-mcp-add-help.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/04-codex-mcp-add-help.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24/04-codex-mcp-add-help.out.txt b/docs/uat/evidence/2026-02-24/04-codex-mcp-add-help.out.txt new file mode 100644 index 0000000..0e8cb52 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/04-codex-mcp-add-help.out.txt @@ -0,0 +1,36 @@ +Usage: codex mcp add [OPTIONS] (--url | -- ...) + +Arguments: + + Name for the MCP server configuration + + [COMMAND]... + Command to launch the MCP server. Use --url for a streamable HTTP server + +Options: + -c, --config + Override a configuration value that would otherwise be loaded from `~/.codex/config.toml`. + Use a dotted path (`foo.bar.baz`) to override nested values. The `value` portion is parsed + as TOML. If it fails to parse as TOML, the raw string is used as a literal. + + Examples: - `-c model="o3"` - `-c 'sandbox_permissions=["disk-full-read-access"]'` - `-c + shell_environment_policy.inherit=all` + + --env + Environment variables to set when launching the server. Only valid with stdio servers + + --enable + Enable a feature (repeatable). Equivalent to `-c features.=true` + + --url + URL for a streamable HTTP MCP server + + --bearer-token-env-var + Optional environment variable to read for a bearer token. Only valid with streamable HTTP + servers + + --disable + Disable a feature (repeatable). Equivalent to `-c features.=false` + + -h, --help + Print help (see a summary with '-h') diff --git a/docs/uat/evidence/2026-02-24/05-codex-login-status.cmd.txt b/docs/uat/evidence/2026-02-24/05-codex-login-status.cmd.txt new file mode 100644 index 0000000..b463ad9 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/05-codex-login-status.cmd.txt @@ -0,0 +1 @@ +codex login status diff --git a/docs/uat/evidence/2026-02-24/05-codex-login-status.exit b/docs/uat/evidence/2026-02-24/05-codex-login-status.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/05-codex-login-status.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24/05-codex-login-status.out.txt b/docs/uat/evidence/2026-02-24/05-codex-login-status.out.txt new file mode 100644 index 0000000..39390a3 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/05-codex-login-status.out.txt @@ -0,0 +1 @@ +Logged in using ChatGPT diff --git a/docs/uat/evidence/2026-02-24/06-env-plan.cmd.txt b/docs/uat/evidence/2026-02-24/06-env-plan.cmd.txt new file mode 100644 index 0000000..2d3ed05 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/06-env-plan.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js env plan mcp-filesystem mcp-playwright mcp-desktop-commander diff --git a/docs/uat/evidence/2026-02-24/06-env-plan.exit b/docs/uat/evidence/2026-02-24/06-env-plan.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/06-env-plan.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24/06-env-plan.out.txt b/docs/uat/evidence/2026-02-24/06-env-plan.out.txt new file mode 100644 index 0000000..834c222 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/06-env-plan.out.txt @@ -0,0 +1,24 @@ +⚙️ Environment variables loaded from local .env file(s) (1). +🔒 `src/cli/.env` is local-sensitive state. Avoid sharing logs/screenshots with local env details; set CODEX_CLI_ENV_AUTOLOAD=0 to disable auto-loading. + +mcp-filesystem + description: Local filesystem MCP server (read-only by default) + compose: docker/mcp/docker-compose.filesystem.yml + services: mcp-filesystem + port: 7040 + codex mcp name: filesystem-local + filesystem mode: read-only (default) or controlled-write with explicit approval + +mcp-playwright + description: Playwright automation MCP server + compose: docker/mcp/docker-compose.playwright.yml + services: mcp-playwright + port: 7030 + codex mcp name: playwright-local + +mcp-desktop-commander + description: Desktop Commander MCP server for desktop/tooling automation + compose: docker/mcp/docker-compose.desktop-commander.yml + services: mcp-desktop-commander + port: 7070 + codex mcp name: desktop-commander diff --git a/docs/uat/evidence/2026-02-24/07-env-docker-login.cmd.txt b/docs/uat/evidence/2026-02-24/07-env-docker-login.cmd.txt new file mode 100644 index 0000000..e589dd2 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/07-env-docker-login.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js env docker-login mcp-filesystem mcp-playwright mcp-desktop-commander diff --git a/docs/uat/evidence/2026-02-24/07-env-docker-login.exit b/docs/uat/evidence/2026-02-24/07-env-docker-login.exit new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/07-env-docker-login.exit @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24/07-env-docker-login.out.txt b/docs/uat/evidence/2026-02-24/07-env-docker-login.out.txt new file mode 100644 index 0000000..18099e9 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/07-env-docker-login.out.txt @@ -0,0 +1,5 @@ +⚙️ Environment variables loaded from local .env file(s) (1). +🔒 `src/cli/.env` is local-sensitive state. Avoid sharing logs/screenshots with local env details; set CODEX_CLI_ENV_AUTOLOAD=0 to disable auto-loading. +2026-02-24T14:27:00.682Z ✨ INFO [env] Authenticating Docker registry ~ all green—smooth sailing ✨ | data: registry=ghcr.io +error: cannot perform an interactive login from a non-TTY device +❌ env.docker-login failed: Command failed: docker login ghcr.io diff --git a/docs/uat/evidence/2026-02-24/08-env-up.cmd.txt b/docs/uat/evidence/2026-02-24/08-env-up.cmd.txt new file mode 100644 index 0000000..39e36c3 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/08-env-up.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js env up mcp-filesystem mcp-playwright mcp-desktop-commander diff --git a/docs/uat/evidence/2026-02-24/08-env-up.exit b/docs/uat/evidence/2026-02-24/08-env-up.exit new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/08-env-up.exit @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24/08-env-up.out.txt b/docs/uat/evidence/2026-02-24/08-env-up.out.txt new file mode 100644 index 0000000..1a953d8 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/08-env-up.out.txt @@ -0,0 +1,9 @@ +⚙️ Environment variables loaded from local .env file(s) (1). +🔒 `src/cli/.env` is local-sensitive state. Avoid sharing logs/screenshots with local env details; set CODEX_CLI_ENV_AUTOLOAD=0 to disable auto-loading. +2026-02-24T14:27:01.165Z ✨ INFO [env] Starting service mcp-filesystem ~ all green—smooth sailing ✨ | data: command=docker compose -f docker/mcp/docker-c… +❌ env.up failed: Docker image pull/auth denied for mcp-filesystem (ghcr.io/context-labs/filesystem-mcp:latest) (exit=1, compose=docker compose -f docker/mcp/docker-compose.filesystem.yml up -d mcp-filesystem). Run `codex-synaptic env docker-login mcp-filesystem` and retry `codex-synaptic env up mcp-filesystem`. Raw docker output: time="2026-02-24T08:27:01-06:00" level=warning msg="/Users/chrisdukes/LocalProjects/codex-synaptic/docker/mcp/docker-compose.filesystem.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion" + Image ghcr.io/context-labs/filesystem-mcp:latest Pulling + Image ghcr.io/context-labs/filesystem-mcp:latest Error error from registry: denied +denied +Error response from daemon: error from registry: denied +denied diff --git a/docs/uat/evidence/2026-02-24/09-env-status.cmd.txt b/docs/uat/evidence/2026-02-24/09-env-status.cmd.txt new file mode 100644 index 0000000..86076b2 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/09-env-status.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js env status mcp-filesystem mcp-playwright mcp-desktop-commander diff --git a/docs/uat/evidence/2026-02-24/09-env-status.exit b/docs/uat/evidence/2026-02-24/09-env-status.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/09-env-status.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24/09-env-status.out.txt b/docs/uat/evidence/2026-02-24/09-env-status.out.txt new file mode 100644 index 0000000..0dc3986 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/09-env-status.out.txt @@ -0,0 +1,20 @@ +⚙️ Environment variables loaded from local .env file(s) (1). +🔒 `src/cli/.env` is local-sensitive state. Avoid sharing logs/screenshots with local env details; set CODEX_CLI_ENV_AUTOLOAD=0 to disable auto-loading. + +mcp-filesystem + running: no + healthy: no + checkedAt: 2026-02-24T14:27:03.403Z +NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS + +mcp-playwright + running: no + healthy: no + checkedAt: 2026-02-24T14:27:03.478Z +NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS + +mcp-desktop-commander + running: no + healthy: no + checkedAt: 2026-02-24T14:27:03.549Z +NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS diff --git a/docs/uat/evidence/2026-02-24/10-env-codex-register.cmd.txt b/docs/uat/evidence/2026-02-24/10-env-codex-register.cmd.txt new file mode 100644 index 0000000..cd3eac6 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/10-env-codex-register.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js env codex-register mcp-filesystem mcp-playwright mcp-desktop-commander --replace diff --git a/docs/uat/evidence/2026-02-24/10-env-codex-register.exit b/docs/uat/evidence/2026-02-24/10-env-codex-register.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/10-env-codex-register.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24/10-env-codex-register.out.txt b/docs/uat/evidence/2026-02-24/10-env-codex-register.out.txt new file mode 100644 index 0000000..6819610 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/10-env-codex-register.out.txt @@ -0,0 +1,8 @@ +⚙️ Environment variables loaded from local .env file(s) (1). +🔒 `src/cli/.env` is local-sensitive state. Avoid sharing logs/screenshots with local env details; set CODEX_CLI_ENV_AUTOLOAD=0 to disable auto-loading. +Removed existing Codex MCP entry: filesystem-local +✅ Registered Codex MCP server filesystem-local -> http://localhost:7040 +Removed existing Codex MCP entry: playwright-local +✅ Registered Codex MCP server playwright-local -> http://localhost:7030 +Removed existing Codex MCP entry: desktop-commander +✅ Registered Codex MCP server desktop-commander -> http://localhost:7070 diff --git a/docs/uat/evidence/2026-02-24/11-codex-mcp-list.cmd.txt b/docs/uat/evidence/2026-02-24/11-codex-mcp-list.cmd.txt new file mode 100644 index 0000000..fbbaaa8 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/11-codex-mcp-list.cmd.txt @@ -0,0 +1 @@ +codex mcp list --json diff --git a/docs/uat/evidence/2026-02-24/11-codex-mcp-list.err.txt b/docs/uat/evidence/2026-02-24/11-codex-mcp-list.err.txt new file mode 100644 index 0000000..e69de29 diff --git a/docs/uat/evidence/2026-02-24/11-codex-mcp-list.exit b/docs/uat/evidence/2026-02-24/11-codex-mcp-list.exit new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/11-codex-mcp-list.exit @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24/12-doctor-strict-json.cmd.txt b/docs/uat/evidence/2026-02-24/12-doctor-strict-json.cmd.txt new file mode 100644 index 0000000..01187f9 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/12-doctor-strict-json.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js doctor --strict --json diff --git a/docs/uat/evidence/2026-02-24/12-doctor-strict-json.err.txt b/docs/uat/evidence/2026-02-24/12-doctor-strict-json.err.txt new file mode 100644 index 0000000..a258054 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/12-doctor-strict-json.err.txt @@ -0,0 +1 @@ +❌ doctor failed: Doctor found 3 failing check(s). diff --git a/docs/uat/evidence/2026-02-24/12-doctor-strict-json.exit b/docs/uat/evidence/2026-02-24/12-doctor-strict-json.exit new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/12-doctor-strict-json.exit @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24/13-launch-strict-json.cmd.txt b/docs/uat/evidence/2026-02-24/13-launch-strict-json.cmd.txt new file mode 100644 index 0000000..c9e5c45 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/13-launch-strict-json.cmd.txt @@ -0,0 +1 @@ +node dist/cli/index.js launch --strict --json diff --git a/docs/uat/evidence/2026-02-24/13-launch-strict-json.err.txt b/docs/uat/evidence/2026-02-24/13-launch-strict-json.err.txt new file mode 100644 index 0000000..eda2d11 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/13-launch-strict-json.err.txt @@ -0,0 +1 @@ +❌ launch failed: Launch failed one or more readiness gates. diff --git a/docs/uat/evidence/2026-02-24/13-launch-strict-json.exit b/docs/uat/evidence/2026-02-24/13-launch-strict-json.exit new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/13-launch-strict-json.exit @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/docs/uat/evidence/2026-02-24/_status.tsv b/docs/uat/evidence/2026-02-24/_status.tsv new file mode 100644 index 0000000..9b686e5 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/_status.tsv @@ -0,0 +1,14 @@ +step exit artifact +01-build 0 01-build.out.txt +02-codex-help 0 02-codex-help.out.txt +03-codex-mcp-help 0 03-codex-mcp-help.out.txt +04-codex-mcp-add-help 0 04-codex-mcp-add-help.out.txt +05-codex-login-status 0 05-codex-login-status.out.txt +06-env-plan 0 06-env-plan.out.txt +07-env-docker-login 1 07-env-docker-login.out.txt +08-env-up 1 08-env-up.out.txt +09-env-status 0 09-env-status.out.txt +10-env-codex-register 0 10-env-codex-register.out.txt +11-codex-mcp-list 0 codex.mcp.list.json +12-doctor-strict-json 1 doctor.strict.json +13-launch-strict-json 1 launch.strict.json diff --git a/docs/uat/evidence/2026-02-24/codex.mcp.list.json b/docs/uat/evidence/2026-02-24/codex.mcp.list.json new file mode 100644 index 0000000..79953a2 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/codex.mcp.list.json @@ -0,0 +1,125 @@ +[ + { + "name": "desktop-commander", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "http://localhost:7070", + "bearer_token_env_var": null, + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "unsupported" + }, + { + "name": "figma", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "https://mcp.figma.com/mcp", + "bearer_token_env_var": null, + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "o_auth" + }, + { + "name": "filesystem-local", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "http://localhost:7040", + "bearer_token_env_var": null, + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "unsupported" + }, + { + "name": "linear", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "https://mcp.linear.app/mcp", + "bearer_token_env_var": "REDACTED_LOCAL_SECRET", + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "bearer_token" + }, + { + "name": "notion", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "https://mcp.notion.com/mcp", + "bearer_token_env_var": null, + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "o_auth" + }, + { + "name": "openaiDeveloperDocs", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "https://developers.openai.com/mcp", + "bearer_token_env_var": null, + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "unsupported" + }, + { + "name": "playwright", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "stdio", + "command": "npx", + "args": [ + "@playwright/mcp@latest" + ], + "env": null, + "env_vars": [], + "cwd": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "unsupported" + }, + { + "name": "playwright-local", + "enabled": true, + "disabled_reason": null, + "transport": { + "type": "streamable_http", + "url": "http://localhost:7030", + "bearer_token_env_var": null, + "http_headers": null, + "env_http_headers": null + }, + "startup_timeout_sec": null, + "tool_timeout_sec": null, + "auth_status": "unsupported" + } +] diff --git a/docs/uat/evidence/2026-02-24/doctor.strict.json b/docs/uat/evidence/2026-02-24/doctor.strict.json new file mode 100644 index 0000000..b39c38d --- /dev/null +++ b/docs/uat/evidence/2026-02-24/doctor.strict.json @@ -0,0 +1,60 @@ +{ + "ok": false, + "summary": { + "passed": 4, + "failed": 3, + "total": 7 + }, + "checks": [ + { + "id": "repo.cli_build_artifact", + "ok": true, + "details": "Found /Users/chrisdukes/LocalProjects/codex-synaptic/dist/cli/index.js" + }, + { + "id": "repo.cli_exec", + "ok": true, + "details": "CLI help command succeeded." + }, + { + "id": "codex.auth", + "ok": true, + "details": "Logged in using ChatGPT" + }, + { + "id": "codex.mcp_list", + "ok": true, + "details": "Loaded 8 Codex MCP registration(s)." + }, + { + "id": "mcp.mcp-filesystem", + "ok": false, + "details": "running=false healthy=false registered=true", + "remediation": "codex-synaptic env docker-login mcp-filesystem && codex-synaptic env up mcp-filesystem", + "metadata": { + "codexName": "filesystem-local", + "url": "http://localhost:7040" + } + }, + { + "id": "mcp.mcp-playwright", + "ok": false, + "details": "running=false healthy=false registered=true", + "remediation": "codex-synaptic env docker-login mcp-playwright && codex-synaptic env up mcp-playwright", + "metadata": { + "codexName": "playwright-local", + "url": "http://localhost:7030" + } + }, + { + "id": "mcp.mcp-desktop-commander", + "ok": false, + "details": "running=false healthy=false registered=true", + "remediation": "codex-synaptic env docker-login mcp-desktop-commander && codex-synaptic env up mcp-desktop-commander", + "metadata": { + "codexName": "desktop-commander", + "url": "http://localhost:7070" + } + } + ] +} diff --git a/docs/uat/evidence/2026-02-24/launch.strict.json b/docs/uat/evidence/2026-02-24/launch.strict.json new file mode 100644 index 0000000..da5a0bd --- /dev/null +++ b/docs/uat/evidence/2026-02-24/launch.strict.json @@ -0,0 +1,41 @@ +2026-02-24T14:27:06.804Z ✨ INFO [env] Starting service mcp-filesystem ~ all green—smooth sailing ✨ | data: command=docker compose -f docker/mcp/docker-c… +{ + "ok": false, + "steps": [ + { + "id": "repo.preflight", + "ok": true, + "details": "Found /Users/chrisdukes/LocalProjects/codex-synaptic/dist/cli/index.js; CLI executable check passed." + }, + { + "id": "codex.auth", + "ok": true, + "details": "Logged in using ChatGPT" + }, + { + "id": "runtime.daemon", + "ok": true, + "details": "Background daemon already running (pid 64145)." + }, + { + "id": "mcp.up", + "ok": false, + "details": "Failed to start MCP profile mcp-filesystem after starting 0/3: Docker image pull/auth denied for mcp-filesystem (ghcr.io/context-labs/filesystem-mcp:latest) (exit=1, compose=docker compose -f docker/mcp/docker-compose.filesystem.yml up -d mcp-filesystem). Run `codex-synaptic env docker-login mcp-filesystem` and retry `codex-synaptic env up mcp-filesystem`. Raw docker output: time=\"2026-02-24T08:27:06-06:00\" level=warning msg=\"/Users/chrisdukes/LocalProjects/codex-synaptic/docker/mcp/docker-compose.filesystem.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion\"\n Image ghcr.io/context-labs/filesystem-mcp:latest Pulling \n Image ghcr.io/context-labs/filesystem-mcp:latest Error error from registry: denied\ndenied\nError response from daemon: error from registry: denied\ndenied", + "remediation": "codex-synaptic env docker-login mcp-filesystem && codex-synaptic env up mcp-filesystem && codex-synaptic env codex-register mcp-filesystem --replace && codex-synaptic env status mcp-filesystem", + "metadata": { + "failedProfile": "mcp-filesystem", + "startedProfiles": [] + } + } + ], + "doctor": { + "ok": false, + "summary": { + "passed": 0, + "failed": 0, + "total": 0 + }, + "checks": [] + }, + "nextAction": "stop" +} diff --git a/docs/uat/evidence/2026-02-24/launch.strict.payload.json b/docs/uat/evidence/2026-02-24/launch.strict.payload.json new file mode 100644 index 0000000..c130891 --- /dev/null +++ b/docs/uat/evidence/2026-02-24/launch.strict.payload.json @@ -0,0 +1,40 @@ +{ + "ok": false, + "steps": [ + { + "id": "repo.preflight", + "ok": true, + "details": "Found /Users/chrisdukes/LocalProjects/codex-synaptic/dist/cli/index.js; CLI executable check passed." + }, + { + "id": "codex.auth", + "ok": true, + "details": "Logged in using ChatGPT" + }, + { + "id": "runtime.daemon", + "ok": true, + "details": "Background daemon already running (pid 64145)." + }, + { + "id": "mcp.up", + "ok": false, + "details": "Failed to start MCP profile mcp-filesystem after starting 0/3: Docker image pull/auth denied for mcp-filesystem (ghcr.io/context-labs/filesystem-mcp:latest) (exit=1, compose=docker compose -f docker/mcp/docker-compose.filesystem.yml up -d mcp-filesystem). Run `codex-synaptic env docker-login mcp-filesystem` and retry `codex-synaptic env up mcp-filesystem`. Raw docker output: time=\"2026-02-24T08:27:06-06:00\" level=warning msg=\"/Users/chrisdukes/LocalProjects/codex-synaptic/docker/mcp/docker-compose.filesystem.yml: the attribute `version` is obsolete, it will be ignored, please remove it to avoid potential confusion\"\n Image ghcr.io/context-labs/filesystem-mcp:latest Pulling \n Image ghcr.io/context-labs/filesystem-mcp:latest Error error from registry: denied\ndenied\nError response from daemon: error from registry: denied\ndenied", + "remediation": "codex-synaptic env docker-login mcp-filesystem && codex-synaptic env up mcp-filesystem && codex-synaptic env codex-register mcp-filesystem --replace && codex-synaptic env status mcp-filesystem", + "metadata": { + "failedProfile": "mcp-filesystem", + "startedProfiles": [] + } + } + ], + "doctor": { + "ok": false, + "summary": { + "passed": 0, + "failed": 0, + "total": 0 + }, + "checks": [] + }, + "nextAction": "stop" +} diff --git a/docs/uat/evidence/2026-02-24/launch.strict.stdout-prefix.txt b/docs/uat/evidence/2026-02-24/launch.strict.stdout-prefix.txt new file mode 100644 index 0000000..612f57d --- /dev/null +++ b/docs/uat/evidence/2026-02-24/launch.strict.stdout-prefix.txt @@ -0,0 +1 @@ +2026-02-24T14:27:06.804Z ✨ INFO [env] Starting service mcp-filesystem ~ all green—smooth sailing ✨ | data: command=docker compose -f docker/mcp/docker-c… diff --git a/package-lock.json b/package-lock.json index 7c0a028..83663b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "AGPL-3.0-only", "dependencies": { + "@openai/agents": "^0.1.11", "chalk": "^5.6.2", "commander": "^14.0.0", "crypto-js": "^4.2.0", @@ -670,6 +671,19 @@ "license": "MIT", "optional": true }, + "node_modules/@hono/node-server": { + "version": "1.19.9", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", + "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -1156,6 +1170,99 @@ "dev": true, "license": "MIT" }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.27.0.tgz", + "integrity": "sha512-qOdO524oPMkUsOJTrsH9vz/HN3B5pKyW+9zIW51A9kDMVe7ON70drz1ouoyoyOcfzc+oxhkQ6jWmbyKnlWmYqA==", + "license": "MIT", + "optional": true, + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "optional": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "node_modules/@jridgewell/trace-mapping": { "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", @@ -1193,6 +1300,135 @@ "node": ">=10" } }, + "node_modules/@openai/agents": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@openai/agents/-/agents-0.1.11.tgz", + "integrity": "sha512-jnaFt54iP71vYDXvpG3EGX2kVRYIU2xBdCT3uFqdXm4KqFAP9JQFNGiKKBEeE5rbXARpqAQpKH+5HfoANndpcQ==", + "license": "MIT", + "dependencies": { + "@openai/agents-core": "0.1.11", + "@openai/agents-openai": "0.1.11", + "@openai/agents-realtime": "0.1.11", + "debug": "^4.4.0", + "openai": "^5.20.2" + }, + "peerDependencies": { + "zod": "^3.25.40" + } + }, + "node_modules/@openai/agents-core": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@openai/agents-core/-/agents-core-0.1.11.tgz", + "integrity": "sha512-ye8VIAO2wPIg1zClldIj8We/1R55VmdgnMyn0g4YGbp6RD5Wpv9yfH5kPNWxmHvw8ji+XehyggGoklY4FGQoBQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "openai": "^5.20.2" + }, + "optionalDependencies": { + "@modelcontextprotocol/sdk": "^1.17.2" + }, + "peerDependencies": { + "zod": "^3.25.40" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@openai/agents-core/node_modules/openai": { + "version": "5.23.2", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.23.2.tgz", + "integrity": "sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@openai/agents-openai": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@openai/agents-openai/-/agents-openai-0.1.11.tgz", + "integrity": "sha512-TYYbY7o1cNxtOIO4F1a20qkqDT1Iwr9ZEi0MO2sEaeioK8rGB/Ux54iFvsA3IblUYyK+5fD25vr4vXFasvg2Kg==", + "license": "MIT", + "dependencies": { + "@openai/agents-core": "0.1.11", + "debug": "^4.4.0", + "openai": "^5.20.2" + }, + "peerDependencies": { + "zod": "^3.25.40" + } + }, + "node_modules/@openai/agents-openai/node_modules/openai": { + "version": "5.23.2", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.23.2.tgz", + "integrity": "sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@openai/agents-realtime": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/@openai/agents-realtime/-/agents-realtime-0.1.11.tgz", + "integrity": "sha512-8jaNuYU1acra28i7bYrZIPubI6s2ziY2ZudqAVK2ad+giopXcrNSiJTuZ2S3z+ESnIejwMiYLfnY2Le8W0SJ7A==", + "license": "MIT", + "dependencies": { + "@openai/agents-core": "0.1.11", + "@types/ws": "^8.18.1", + "debug": "^4.4.0", + "ws": "^8.18.1" + }, + "peerDependencies": { + "zod": "^3.25.40" + } + }, + "node_modules/@openai/agents/node_modules/openai": { + "version": "5.23.2", + "resolved": "https://registry.npmjs.org/openai/-/openai-5.23.2.tgz", + "integrity": "sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==", + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.57.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", @@ -2104,6 +2340,48 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "license": "MIT", + "optional": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT", + "optional": true + }, "node_modules/ansi-escapes": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", @@ -2271,6 +2549,48 @@ "readable-stream": "^3.4.0" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "optional": true, + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/brace-expansion": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", @@ -2613,6 +2933,30 @@ "license": "ISC", "optional": true }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-to-spaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/convert-to-spaces/-/convert-to-spaces-2.0.1.tgz", @@ -2622,6 +2966,44 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "optional": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -3202,6 +3584,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/express-rate-limit": { + "version": "8.2.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", + "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", "node_modules/execa/node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -3209,6 +3595,10 @@ "dev": true, "license": "MIT", "dependencies": { + "ip-address": "10.0.1" + }, + "engines": { + "node": ">= 16" "mimic-fn": "^4.0.0" }, "engines": { @@ -3261,6 +3651,33 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "optional": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3293,6 +3710,28 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -3568,6 +4007,27 @@ "license": "BSD-2-Clause", "optional": true }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/http-proxy-agent": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", @@ -3924,6 +4384,16 @@ "devOptional": true, "license": "ISC" }, + "node_modules/jose": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.1.3.tgz", + "integrity": "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3956,6 +4426,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause", + "optional": true + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", @@ -4114,6 +4591,57 @@ "dev": true, "license": "MIT" }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "optional": true, + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -4719,6 +5247,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=16.20.0" + } + }, "node_modules/pkg-types": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", @@ -4895,6 +5433,79 @@ "node": ">=6" } }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "optional": true, + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/rc": { "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", @@ -4982,6 +5593,16 @@ "node": ">=8.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -5145,6 +5766,53 @@ "node": ">=10" } }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "optional": true, + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -6190,6 +6858,7 @@ "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "license": "MIT", + "peer": true, "engines": { "node": ">=10.0.0" }, @@ -6262,6 +6931,16 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "optional": true, + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/package.json b/package.json index 22c08a5..fb21330 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,18 @@ "bin": { "codex-synaptic": "dist/cli/index.js" }, + "files": [ + "dist/", + "docker/", + "config/", + ".env.example", + "README.md", + "AGENTS.md", + "CHANGELOG.md", + "LICENSE", + "docs/codex-synaptic-cheat-codes.md", + ".codex-improvement/SCHEMA_MASTER.yaml" + ], "scripts": { "build": "tsc && node scripts/dev-cli-link.mjs", "dev": "ts-node src/index.ts", @@ -54,6 +66,7 @@ "vitest": "^1.6.1" }, "dependencies": { + "@openai/agents": "^0.1.11", "chalk": "^5.6.2", "commander": "^14.0.0", "crypto-js": "^4.2.0", diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts new file mode 100644 index 0000000..69def5e --- /dev/null +++ b/src/cli/doctor.ts @@ -0,0 +1,318 @@ +import { spawn } from 'child_process'; +import { existsSync } from 'fs'; +import { join } from 'path'; +import { serviceManager, type ServiceStatus } from '../env/service-manager.js'; +import { BridgeError, ErrorCode } from '../core/errors.js'; + +export enum MCPProfile { + Filesystem = 'mcp-filesystem', + Playwright = 'mcp-playwright', + DesktopCommander = 'mcp-desktop-commander' +} + +export const DEFAULT_MCP_PROFILES = Object.values(MCPProfile); + +export type SpawnCommandResult = { + status: number | null; + stdout: string; + stderr: string; +}; + +export interface DoctorCheck { + id: string; + ok: boolean; + details: string; + remediation?: string; + metadata?: Record; +} + +export interface DoctorSummary { + passed: number; + failed: number; + total: number; +} + +export interface DoctorReport { + ok: boolean; + summary: DoctorSummary; + checks: DoctorCheck[]; +} + +export interface DoctorOptions { + cwd?: string; + mcpProfiles?: string[]; + skipCodexAuth?: boolean; +} + +export interface DoctorDependencies { + fileExists?: (path: string) => boolean | Promise; + spawnCommand?: ( + command: string, + args: string[], + options: { cwd: string; encoding: BufferEncoding } + ) => Promise; + getServiceStatus?: (name: string) => Promise; + getCodexRegistration?: (name: string) => { codexName: string; url: string } | null; + registriesForProfiles?: (names: string[]) => string[]; +} + +function parseCodexMcpNames(payload: unknown): string[] { + if (Array.isArray(payload)) { + return payload + .map((entry) => { + if (!entry || typeof entry !== 'object') { + return undefined; + } + return String((entry as { name?: string }).name ?? ''); + }) + .filter(Boolean) as string[]; + } + + if (payload && typeof payload === 'object') { + const candidateArrays = [ + (payload as { servers?: unknown }).servers, + (payload as { items?: unknown }).items, + (payload as { mcpServers?: unknown }).mcpServers + ]; + + for (const candidate of candidateArrays) { + if (!Array.isArray(candidate)) { + continue; + } + + return candidate + .map((entry) => { + if (!entry || typeof entry !== 'object') { + return undefined; + } + return String((entry as { name?: string }).name ?? ''); + }) + .filter(Boolean) as string[]; + } + } + + throw new BridgeError( + ErrorCode.MCP_ERROR, + 'Unsupported JSON format returned by `codex mcp list --json`.', + { retryable: false } + ); +} + +export function parseProfileList(input: string | string[] | undefined, fallback = [...DEFAULT_MCP_PROFILES]): string[] { + if (Array.isArray(input)) { + const normalized = input + .map((item) => item.trim()) + .filter(Boolean); + return normalized.length ? normalized : [...fallback]; + } + + if (typeof input === 'string') { + const normalized = input + .split(',') + .map((item) => item.trim()) + .filter(Boolean); + return normalized.length ? normalized : [...fallback]; + } + + return [...fallback]; +} + +export function collectDoctorRemediations(report: DoctorReport): string[] { + const unique = new Set(); + + for (const check of report.checks) { + if (check.ok || !check.remediation) { + continue; + } + + const commands = check.remediation + .split('&&') + .map((item) => item.trim()) + .filter(Boolean); + + for (const command of commands) { + unique.add(command); + } + } + + return Array.from(unique); +} + +export async function runDoctor(options: DoctorOptions = {}, deps: DoctorDependencies = {}): Promise { + const cwd = options.cwd ?? process.cwd(); + const profileNames = parseProfileList(options.mcpProfiles); + const fileExists = deps.fileExists ?? existsSync; + const spawnCommand = deps.spawnCommand + ?? ((command, args, spawnOptions) => new Promise((resolve) => { + const child = spawn(command, args, { + cwd: spawnOptions.cwd, + stdio: ['ignore', 'pipe', 'pipe'] + }); + if (child.stdout) { + child.stdout.setEncoding(spawnOptions.encoding); + } + if (child.stderr) { + child.stderr.setEncoding(spawnOptions.encoding); + } + + let stdout = ''; + let stderr = ''; + child.stdout?.on('data', (chunk: string) => { + stdout += chunk; + }); + child.stderr?.on('data', (chunk: string) => { + stderr += chunk; + }); + child.on('close', (status) => { + resolve({ status, stdout, stderr }); + }); + child.on('error', (error) => { + resolve({ + status: 1, + stdout, + stderr: stderr || `${error.name}: ${error.message}` + }); + }); + })); + const getServiceStatus = deps.getServiceStatus ?? ((name: string) => serviceManager.status(name)); + const getCodexRegistration = deps.getCodexRegistration + ?? ((name: string) => serviceManager.codexRegistration(name)); + const registriesForProfiles = deps.registriesForProfiles + ?? ((names: string[]) => serviceManager.registriesForProfiles(names)); + + const checks: DoctorCheck[] = []; + + const distCliPath = join(cwd, 'dist', 'cli', 'index.js'); + const distExists = await fileExists(distCliPath); + checks.push({ + id: 'repo.cli_build_artifact', + ok: distExists, + details: distExists ? `Found ${distCliPath}` : `Missing ${distCliPath}`, + remediation: distExists ? undefined : 'Run `npm run build`.' + }); + + if (distExists) { + const cliHelp = await spawnCommand('node', [distCliPath, '--help'], { + cwd, + encoding: 'utf8' + }); + + checks.push({ + id: 'repo.cli_exec', + ok: cliHelp.status === 0, + details: cliHelp.status === 0 + ? 'CLI help command succeeded.' + : (cliHelp.stderr?.trim() || 'CLI help command failed.'), + remediation: cliHelp.status === 0 + ? undefined + : 'Run `npm run build` and re-run `node dist/cli/index.js --help`.' + }); + } + + if (!options.skipCodexAuth) { + const loginStatus = await spawnCommand('codex', ['login', 'status'], { + cwd, + encoding: 'utf8' + }); + + const stdout = loginStatus.stdout?.trim() || ''; + const ok = loginStatus.status === 0 && !/not logged in/i.test(stdout); + + checks.push({ + id: 'codex.auth', + ok, + details: stdout || loginStatus.stderr?.trim() || 'No output', + remediation: ok ? undefined : 'Run `codex login` then re-run `codex login status`.' + }); + } + + const codexMcpList = await spawnCommand('codex', ['mcp', 'list', '--json'], { + cwd, + encoding: 'utf8' + }); + + let codexMcpNames = new Set(); + if (codexMcpList.status === 0) { + try { + const parsed = JSON.parse(codexMcpList.stdout || '[]') as unknown; + const names = parseCodexMcpNames(parsed); + codexMcpNames = new Set(names); + checks.push({ + id: 'codex.mcp_list', + ok: true, + details: `Loaded ${codexMcpNames.size} Codex MCP registration(s).` + }); + } catch (error) { + checks.push({ + id: 'codex.mcp_list', + ok: false, + details: `Failed to parse codex mcp list output: ${(error as Error).message}`, + remediation: 'Run `codex mcp list --json` and inspect output.' + }); + } + } else { + checks.push({ + id: 'codex.mcp_list', + ok: false, + details: codexMcpList.stderr?.trim() || 'codex mcp list failed', + remediation: 'Verify Codex CLI install and MCP support (`codex mcp --help`).' + }); + } + + for (const profileName of profileNames) { + try { + const status = await getServiceStatus(profileName); + const registration = getCodexRegistration(profileName); + const registered = registration ? codexMcpNames.has(registration.codexName) : true; + const healthy = status.healthy !== false; + const ok = status.running && healthy && registered; + + let details = `running=${status.running} healthy=${status.healthy === null ? 'n/a' : status.healthy} registered=${registered}`; + if (status.diagnostics.length) { + details += ` diagnostics=${status.diagnostics.join(' | ')}`; + } + + const remediationParts: string[] = []; + if (!status.running || !healthy) { + if (registriesForProfiles([profileName]).length > 0) { + remediationParts.push(`codex-synaptic env docker-login ${profileName}`); + } + remediationParts.push(`codex-synaptic env up ${profileName}`); + } + if (registration && !registered) { + remediationParts.push(`codex-synaptic env codex-register ${profileName}`); + } + + checks.push({ + id: `mcp.${profileName}`, + ok, + details, + remediation: remediationParts.length ? remediationParts.join(' && ') : undefined, + metadata: { + codexName: registration?.codexName, + url: registration?.url + } + }); + } catch (error) { + checks.push({ + id: `mcp.${profileName}`, + ok: false, + details: `invalid profile: ${(error as Error).message}`, + remediation: `Verify MCP profile name "${profileName}" and retry.` + }); + } + } + + const passed = checks.filter((check) => check.ok).length; + const failed = checks.length - passed; + + return { + ok: failed === 0, + summary: { + passed, + failed, + total: checks.length + }, + checks + }; +} diff --git a/src/cli/env-bootstrap.ts b/src/cli/env-bootstrap.ts new file mode 100644 index 0000000..f274f63 --- /dev/null +++ b/src/cli/env-bootstrap.ts @@ -0,0 +1,184 @@ +import { promises as fs } from "fs"; +import { relative, resolve } from "path"; + +function parseBooleanFlag( + value: string | undefined, + fallback: boolean, +): boolean { + if (value === undefined) { + return fallback; + } + + const normalized = value.trim().toLowerCase(); + if (!normalized) { + return fallback; + } + + if (["1", "true", "yes", "on"].includes(normalized)) { + return true; + } + + if (["0", "false", "no", "off"].includes(normalized)) { + return false; + } + + return fallback; +} + +export function shouldAutoLoadCliEnv( + env: NodeJS.ProcessEnv = process.env, +): boolean { + return parseBooleanFlag(env.CODEX_CLI_ENV_AUTOLOAD, true); +} + +export function shouldShowCliEnvBanner( + options: { + env?: NodeJS.ProcessEnv; + cliSilent?: boolean; + argv?: string[]; + } = {}, +): boolean { + const env = options.env ?? process.env; + const cliSilent = options.cliSilent === true; + const argv = options.argv ?? process.argv; + + if (cliSilent) { + return false; + } + + // Keep JSON stdout clean by default; allow override for debugging. + if ( + argv.includes("--json") && + !parseBooleanFlag(env.CODEX_CLI_ENV_BANNER_FORCE, false) + ) { + return false; + } + + return parseBooleanFlag(env.CODEX_CLI_ENV_BANNER, true); +} + +export async function loadEnvFile( + filePath: string, + env: NodeJS.ProcessEnv = process.env, +): Promise { + try { + await fs.access(filePath); + } catch { + return false; + } + + try { + const content = await fs.readFile(filePath, "utf8"); + const lines = content.split(/\r?\n/); + let applied = false; + + for (const rawLine of lines) { + const line = rawLine.trim(); + if (!line || line.startsWith("#")) { + continue; + } + + const separatorIndex = line.indexOf("="); + if (separatorIndex === -1) { + continue; + } + + const key = line.slice(0, separatorIndex).trim(); + if (!key) { + continue; + } + + let value = line.slice(separatorIndex + 1).trim(); + if (!value) { + value = ""; + } + + const startsWithQuote = value.startsWith('"') || value.startsWith("'"); + const endsWithQuote = value.endsWith('"') || value.endsWith("'"); + if (startsWithQuote && endsWithQuote && value.length >= 2) { + value = value.slice(1, -1); + } + + value = value + .replace(/\\n/g, "\n") + .replace(/\\r/g, "\r") + .replace(/\\t/g, "\t"); + + if (env[key] === undefined) { + env[key] = value; + applied = true; + } + } + + return applied; + } catch { + return false; + } +} + +export async function bootstrapCliEnv( + options: { + cwd?: string; + env?: NodeJS.ProcessEnv; + } = {}, +): Promise { + const cwd = options.cwd ?? process.cwd(); + const env = options.env ?? process.env; + const sources: string[] = []; + + const candidates = [ + resolve(cwd, ".env"), + resolve(cwd, ".env.local"), + resolve(cwd, "src/cli/.env"), + ]; + + const seen = new Set(); + for (const candidate of candidates) { + if (seen.has(candidate)) { + continue; + } + seen.add(candidate); + if (await loadEnvFile(candidate, env)) { + sources.push(candidate); + } + } + + return sources; +} + +export function buildCliEnvBootstrapMessages( + loadedSources: string[], + options: { + cwd?: string; + env?: NodeJS.ProcessEnv; + } = {}, +): string[] { + if (!loadedSources.length) { + return []; + } + + const cwd = options.cwd ?? process.cwd(); + const env = options.env ?? process.env; + const verbosePaths = + parseBooleanFlag(env.CODEX_CLI_ENV_BANNER_VERBOSE, false) || + env.CODEX_DEBUG === "1"; + const loadedSrcCliEnv = loadedSources.some((source) => + /(^|[\\/])src[\\/]cli[\\/]\.env$/.test(source), + ); + + const firstLine = verbosePaths + ? `⚙️ Environment variables loaded from ${loadedSources + .map((source) => relative(cwd, source) || source) + .join(", ")}` + : `⚙️ Environment variables loaded from local .env file(s) (${loadedSources.length}).`; + + const lines = [firstLine]; + + if (loadedSrcCliEnv) { + lines.push( + "🔒 `src/cli/.env` is local-sensitive state. Avoid sharing logs/screenshots with local env details; set CODEX_CLI_ENV_AUTOLOAD=0 to disable auto-loading.", + ); + } + + return lines; +} diff --git a/src/cli/index.ts b/src/cli/index.ts index d87fa0a..4291ed1 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -32,6 +32,7 @@ import type { CodexPromptEnvelope, ContextLogEntry } from '../types/codex-context.js'; +import { CliGateError, ErrorCode, RetryManager } from '../core/errors.js'; import { RetryManager, DaemonConflictError } from '../core/errors.js'; import { HiveMindYamlFormatter } from '../utils/yaml-output.js'; import { parseFileContent, parseJsonInput, loadFileThroughFeedforward } from './feedforward.js'; @@ -77,6 +78,19 @@ import { type QuotaOptions } from './tenant-quota-helpers.js'; import { + DEFAULT_MCP_PROFILES, + parseProfileList, + runDoctor +} from './doctor.js'; +import { collectLaunchRemediations, runLaunch } from './launch.js'; +import { + bootstrapCliEnv, + buildCliEnvBootstrapMessages, + shouldAutoLoadCliEnv, + shouldShowCliEnvBanner +} from './env-bootstrap.js'; + +let loadedEnvSources: string[] = []; executeGoapWorkflow, executeTaskWithConsensus, collectExecutionResults, @@ -229,16 +243,6 @@ if (cliSilent) { rootLogger.setConsoleLevel(LogLevel.ERROR); } -if (!cliSilent && loadedEnvSources.length) { - console.log( - chalk.gray( - `⚙️ Environment variables loaded from ${loadedEnvSources - .map((source) => relative(process.cwd(), source) || source) - .join(', ')}` - ) - ); -} - type BackgroundJob = { id: number; command: string; @@ -5172,6 +5176,34 @@ async function execCodexCommand(args: string[], timeoutMs = 10000): Promise<{ st } } +envCmd + .command('docker-login') + .description('Authenticate Docker registries required by one or more service profiles') + .argument('[names...]', 'Service profile names (defaults to launch gate profiles)') + .option('--dry-run', 'Print docker login commands without executing them') + .action(handleCommand('env.docker-login', async (names: string[] = [], options) => { + const targets = names.length ? names : [...DEFAULT_MCP_PROFILES]; + const registries = serviceManager.registriesForProfiles(targets); + + if (!registries.length) { + console.log(chalk.gray(`No registry authentication required for profiles: ${targets.join(', ')}`)); + return; + } + + if (options.dryRun) { + console.log(chalk.blue('Docker registry login commands (dry-run):')); + registries.forEach((registry) => { + console.log(chalk.gray(` docker login ${registry}`)); + }); + return; + } + + for (const registry of registries) { + await serviceManager.dockerLogin(registry); + console.log(chalk.green(`✅ Docker auth completed for ${registry}`)); + } + })); + envCmd .command('codex-register') .description('Register MCP HTTP profiles in Codex CLI MCP config') @@ -5206,6 +5238,96 @@ envCmd } })); +const launchCmd = decorateCommandHelp( + program + .command('launch') + .description('Start detached runtime and hard-gate readiness before repository work'), + { + title: 'Launch Gate', + subtitle: 'Boot daemon + MCP dependencies and fail-fast if the repo is not work-ready.', + context: [ + 'Launch is the single-command bootstrap for Codex for macOS first prompts.', + 'In strict mode, launch stops on the first failing gate and exits non-zero.' + ], + skills: [ + 'Guarantee daemon, MCP profile, and doctor readiness before edits start.', + 'Emit machine-readable launch reports for automation and handoffs.' + ], + actions: [ + { command: 'codex-synaptic launch --json', description: 'Run the full bootstrap gate and emit structured output.' }, + { command: 'codex-synaptic launch --no-strict --json', description: 'Collect gate results without immediate fail-fast exit.' } + ], + docs: [ + { label: 'docs/guides/codex-macos-workflows.md', description: 'Single-command first-launch flow for Codex for macOS.' } + ] + } +); + +launchCmd + .option('--json', 'Output launch report as JSON') + .option('--strict', 'Exit with an error when any launch gate fails', true) + .option('--no-strict', 'Report failing gates without exiting non-zero') + .option('--skip-codex-auth', 'Skip codex login status check') + .option( + '--mcp-profiles ', + 'Comma-separated MCP service profiles to verify', + DEFAULT_MCP_PROFILES.join(',') + ) + .action(handleCommand('launch', async (options) => { + const strict = options.strict !== false; + const profileNames = parseProfileList(options.mcpProfiles, [...DEFAULT_MCP_PROFILES]); + const report = await runLaunch({ + cwd: process.cwd(), + strict, + skipCodexAuth: Boolean(options.skipCodexAuth), + mcpProfiles: profileNames, + suppressInfoConsoleLogs: Boolean(options.json) + }); + + if (options.json) { + console.log(JSON.stringify(report, null, 2)); + } else { + console.log(chalk.blue('🚀 Codex-Synaptic Launch')); + console.log(chalk.gray(` Status: ${report.ok ? 'ready' : 'blocked'}`)); + console.log(chalk.gray(` Next action: ${report.nextAction}`)); + + report.steps.forEach((step) => { + const marker = step.ok ? chalk.green('✓') : chalk.red('✗'); + console.log(`${marker} ${step.id}: ${step.details}`); + if (!step.ok && step.remediation) { + console.log(chalk.yellow(` remediation: ${step.remediation}`)); + } + }); + + if (report.doctor.summary.total > 0) { + console.log(chalk.gray(` Doctor summary: passed=${report.doctor.summary.passed} failed=${report.doctor.summary.failed}`)); + } else { + console.log(chalk.gray(' Doctor summary: skipped (launch exited before strict doctor run).')); + } + + if (report.ok) { + console.log(chalk.green('✅ Launch gate passed. Safe to begin repository work.')); + } else { + console.log(chalk.red('🛑 Launch gate failed. Stop repository work until remediations pass.')); + const remediation = collectLaunchRemediations(report); + if (remediation.length) { + console.log(chalk.yellow(' Suggested commands:')); + remediation.forEach((command) => { + console.log(chalk.yellow(` - ${command}`)); + }); + } + } + } + + if (strict && !report.ok) { + throw new CliGateError( + ErrorCode.LAUNCH_GATE_FAILURE, + 'Launch failed one or more readiness gates.', + { strict, report } + ); + } + })); + const doctorCmd = decorateCommandHelp( program .command('doctor') @@ -5241,9 +5363,11 @@ doctorCmd .option( '--mcp-profiles ', 'Comma-separated MCP service profiles to verify', - 'mcp-filesystem,mcp-playwright,mcp-desktop-commander' + DEFAULT_MCP_PROFILES.join(',') ) .action(handleCommand('doctor', async (options) => { + const profileNames = parseProfileList(options.mcpProfiles, [...DEFAULT_MCP_PROFILES]); + const report = await runDoctor({ const profileNames = String(options.mcpProfiles) .split(',') .map((item) => item.trim()) @@ -5273,8 +5397,33 @@ doctorCmd let codexMcpNames = new Set(); const codexMcpList = spawnSync('codex', ['mcp', 'list', '--json'], { cwd: process.cwd(), - encoding: 'utf8' + mcpProfiles: profileNames, + skipCodexAuth: Boolean(options.skipCodexAuth) }); + + if (options.json) { + console.log(JSON.stringify(report, null, 2)); + } else { + console.log(chalk.blue('🩺 Codex-Synaptic Doctor')); + console.log(chalk.gray(` Passed: ${report.summary.passed}`)); + console.log(chalk.gray(` Failed: ${report.summary.failed}`)); + + report.checks.forEach((check) => { + const marker = check.ok ? chalk.green('✓') : chalk.red('✗'); + console.log(`${marker} ${check.id}: ${check.details}`); + if (!check.ok && check.remediation) { + console.log(chalk.yellow(` remediation: ${check.remediation}`)); + } + }); + } + + if (options.strict && !report.ok) { + throw new CliGateError( + ErrorCode.DOCTOR_CHECKS_FAILED, + `Doctor found ${report.summary.failed} failing check(s).`, + { summary: report.summary, report } + ); + } if (codexMcpList.status === 0) { try { const parsed = JSON.parse(codexMcpList.stdout || '[]') as Array<{ name?: string }>; @@ -5831,6 +5980,18 @@ program.exitOverride(); // Intercept commands with --codex flag and pass through to Codex CLI // Similar to claude-flow's --claude flag (async () => { + if (shouldAutoLoadCliEnv(process.env)) { + loadedEnvSources = await bootstrapCliEnv({ cwd: process.cwd(), env: process.env }); + } + + if (loadedEnvSources.length && shouldShowCliEnvBanner({ env: process.env, cliSilent, argv: process.argv })) { + const lines = buildCliEnvBootstrapMessages(loadedEnvSources, { cwd: process.cwd(), env: process.env }); + for (const line of lines) { + const colorize = line.startsWith('🔒') ? chalk.yellow : chalk.gray; + process.stderr.write(`${colorize(line)}\n`); + } + } + const args = process.argv.slice(2); // Check if --codex flag is present (but not in hive-mind spawn or cheat which have their own --codex handling) diff --git a/src/cli/launch.ts b/src/cli/launch.ts new file mode 100644 index 0000000..2eae7af --- /dev/null +++ b/src/cli/launch.ts @@ -0,0 +1,489 @@ +import { spawn } from 'child_process'; +import { access } from 'fs/promises'; +import { join } from 'path'; +import { + getBackgroundStatus, + startBackgroundSystem, + type BackgroundStatus +} from './daemon-manager.js'; +import { serviceManager, type EnsureServiceOptions } from '../env/service-manager.js'; +import { + collectDoctorRemediations, + DEFAULT_MCP_PROFILES, + type SpawnCommandResult, + runDoctor, + type DoctorDependencies, + type DoctorOptions, + type DoctorReport +} from './doctor.js'; +import { BridgeError, ErrorCode } from '../core/errors.js'; +import { Logger, LogLevel } from '../core/logger.js'; + +export interface LaunchStep { + id: string; + ok: boolean; + details: string; + remediation?: string; + metadata?: Record; +} + +export interface LaunchReport { + ok: boolean; + steps: LaunchStep[]; + doctor: DoctorReport; + nextAction: LaunchNextAction; +} + +export enum LaunchNextAction { + Continue = 'continue', + Stop = 'stop' +} + +export interface LaunchOptions { + cwd?: string; + strict?: boolean; + skipCodexAuth?: boolean; + mcpProfiles?: string[]; + suppressInfoConsoleLogs?: boolean; +} + +export interface LaunchDependencies extends DoctorDependencies { + startBackground?: () => Promise; + getBackgroundStatus?: () => BackgroundStatus; + ensureService?: (name: string, options?: EnsureServiceOptions) => Promise; + runDoctor?: (options: DoctorOptions, deps?: DoctorDependencies) => Promise; +} + +const EMPTY_DOCTOR_REPORT: DoctorReport = { + ok: false, + summary: { + passed: 0, + failed: 0, + total: 0 + }, + checks: [] +}; + +function normalizeSpawn( + deps: LaunchDependencies +): ( + command: string, + args: string[], + options: { cwd: string; encoding: BufferEncoding } + ) => Promise { + return deps.spawnCommand + ?? ((command, args, spawnOptions) => new Promise((resolve) => { + const child = spawn(command, args, { + cwd: spawnOptions.cwd, + stdio: ['ignore', 'pipe', 'pipe'] + }); + if (child.stdout) { + child.stdout.setEncoding(spawnOptions.encoding); + } + if (child.stderr) { + child.stderr.setEncoding(spawnOptions.encoding); + } + + let stdout = ''; + let stderr = ''; + child.stdout?.on('data', (chunk: string) => { + stdout += chunk; + }); + child.stderr?.on('data', (chunk: string) => { + stderr += chunk; + }); + child.on('close', (status) => { + resolve({ status, stdout, stderr }); + }); + child.on('error', (error) => { + resolve({ + status: 1, + stdout, + stderr: stderr || `${error.name}: ${error.message}` + }); + }); + })); +} + +function buildLaunchReport(steps: LaunchStep[], doctorReport: DoctorReport): LaunchReport { + const ok = steps.every((step) => step.ok) && doctorReport.ok; + return { + ok, + steps, + doctor: doctorReport, + nextAction: ok ? LaunchNextAction.Continue : LaunchNextAction.Stop + }; +} + +function collectLaunchRemediationsFromStep(step: LaunchStep): string[] { + if (!step.remediation) { + return []; + } + + return step.remediation + .split('&&') + .map((item) => item.trim()) + .filter(Boolean); +} + +function buildMcpBootstrapRemediation(profileNames: string[]): string { + const commands: string[] = []; + + commands.push(`codex-synaptic env docker-login ${profileNames.join(' ')}`); + commands.push(`codex-synaptic env up ${profileNames.join(' ')}`); + commands.push(`codex-synaptic env codex-register ${profileNames.join(' ')} --replace`); + + return commands.join(' && '); +} + +async function withSuppressedInfoConsoleLogs(enabled: boolean, work: () => Promise): Promise { + if (!enabled) { + return work(); + } + + const logger = Logger.getInstance(); + const previousConsoleLevel = logger.getConsoleLevel(); + if (previousConsoleLevel >= LogLevel.WARN) { + return work(); + } + + logger.setConsoleLevel(LogLevel.WARN); + try { + return await work(); + } finally { + logger.setConsoleLevel(previousConsoleLevel); + } +} + +export function collectLaunchRemediations(report: LaunchReport): string[] { + const unique = new Set(); + + for (const step of report.steps) { + if (step.ok) { + continue; + } + const commands = collectLaunchRemediationsFromStep(step); + for (const command of commands) { + unique.add(command); + } + } + + for (const command of collectDoctorRemediations(report.doctor)) { + unique.add(command); + } + + return Array.from(unique); +} + +export async function runLaunch(options: LaunchOptions = {}, deps: LaunchDependencies = {}): Promise { + const cwd = options.cwd ?? process.cwd(); + const strict = options.strict !== false; + const profileNames = options.mcpProfiles?.length + ? [...options.mcpProfiles] + : [...DEFAULT_MCP_PROFILES]; + + const fileExists = deps.fileExists ?? (async (path: string) => { + try { + await access(path); + return true; + } catch { + return false; + } + }); + const spawnCommand = normalizeSpawn(deps); + const startBackground = deps.startBackground ?? (() => startBackgroundSystem()); + const readBackgroundStatus = deps.getBackgroundStatus ?? (() => getBackgroundStatus()); + const ensureService = deps.ensureService + ?? ((name: string, ensureOptions?: EnsureServiceOptions) => serviceManager.ensureService(name, ensureOptions)); + const executeDoctor = deps.runDoctor ?? runDoctor; + + const steps: LaunchStep[] = []; + let doctorReport = EMPTY_DOCTOR_REPORT; + + const appendStep = (step: LaunchStep): LaunchReport | null => { + steps.push(step); + if (strict && !step.ok) { + return buildLaunchReport(steps, doctorReport); + } + return null; + }; + + const distCliPath = join(cwd, 'dist', 'cli', 'index.js'); + const distExists = await fileExists(distCliPath); + + let preflightStep: LaunchStep; + if (distExists) { + const cliHelp = await spawnCommand('node', [distCliPath, '--help'], { + cwd, + encoding: 'utf8' + }); + preflightStep = { + id: 'repo.preflight', + ok: cliHelp.status === 0, + details: cliHelp.status === 0 + ? `Found ${distCliPath}; CLI executable check passed.` + : `CLI executable check failed: ${cliHelp.stderr?.trim() || 'unknown error'}`, + remediation: cliHelp.status === 0 + ? undefined + : 'Run `npm run build` and then `node dist/cli/index.js --help`.' + }; + } else { + preflightStep = { + id: 'repo.preflight', + ok: false, + details: `Missing ${distCliPath}`, + remediation: 'Run `npm run build`.' + }; + } + + { + const stop = appendStep(preflightStep); + if (stop) { + return stop; + } + } + + let codexAuthStep: LaunchStep; + if (options.skipCodexAuth) { + codexAuthStep = { + id: 'codex.auth', + ok: true, + details: 'Skipped codex auth check (--skip-codex-auth).' + }; + } else { + const loginStatus = await spawnCommand('codex', ['login', 'status'], { + cwd, + encoding: 'utf8' + }); + const stdout = loginStatus.stdout?.trim() || ''; + const ok = loginStatus.status === 0 && !/not logged in/i.test(stdout); + codexAuthStep = { + id: 'codex.auth', + ok, + details: stdout || loginStatus.stderr?.trim() || 'No output', + remediation: ok ? undefined : 'Run `codex login` then re-run `codex login status`.' + }; + } + + { + const stop = appendStep(codexAuthStep); + if (stop) { + return stop; + } + } + + let daemonStep: LaunchStep; + const existingDaemon = readBackgroundStatus(); + if (existingDaemon.running) { + daemonStep = { + id: 'runtime.daemon', + ok: true, + details: `Background daemon already running (pid ${existingDaemon.pid ?? 'unknown'}).` + }; + } else { + try { + const started = await startBackground(); + daemonStep = { + id: 'runtime.daemon', + ok: started.running, + details: started.running + ? `Background daemon started (pid ${started.pid ?? 'unknown'}).` + : 'Background daemon did not report running state.', + remediation: started.running + ? undefined + : 'Run `codex-synaptic background start` and inspect logs with `codex-synaptic background logs --tail 100`.' + }; + } catch (error) { + daemonStep = { + id: 'runtime.daemon', + ok: false, + details: `Failed to start background daemon: ${(error as Error).message}`, + remediation: 'Run `codex-synaptic background start` and inspect logs with `codex-synaptic background logs --tail 100`.' + }; + } + } + + { + const stop = appendStep(daemonStep); + if (stop) { + return stop; + } + } + + let mcpUpStep: LaunchStep; + if (!profileNames.length) { + mcpUpStep = { + id: 'mcp.up', + ok: true, + details: 'No MCP profiles requested for launch gating.' + }; + } else { + const startedProfiles: string[] = []; + let failedProfile: string | null = null; + let startupError: Error | null = null; + + for (const profileName of profileNames) { + try { + await withSuppressedInfoConsoleLogs(Boolean(options.suppressInfoConsoleLogs), async () => ( + ensureService(profileName, { waitForHealth: true }) + )); + startedProfiles.push(profileName); + } catch (error) { + failedProfile = profileName; + startupError = error as Error; + break; + } + } + + if (!startupError) { + mcpUpStep = { + id: 'mcp.up', + ok: true, + details: `Started ${profileNames.length} MCP profile(s): ${profileNames.join(', ')}` + }; + } else { + const targetedProfiles = failedProfile ? [failedProfile] : profileNames; + const remediationParts = [ + buildMcpBootstrapRemediation(targetedProfiles), + `codex-synaptic env status ${targetedProfiles.join(' ')}` + ]; + + mcpUpStep = { + id: 'mcp.up', + ok: false, + details: `Failed to start MCP profile ${failedProfile ?? 'unknown'} after starting ${startedProfiles.length}/${profileNames.length}: ${startupError.message}`, + remediation: remediationParts.join(' && '), + metadata: { + failedProfile: failedProfile ?? undefined, + startedProfiles + } + }; + } + } + + { + const stop = appendStep(mcpUpStep); + if (stop) { + return stop; + } + } + + let codexRegisterStep: LaunchStep; + if (!profileNames.length) { + codexRegisterStep = { + id: 'mcp.codex_register', + ok: true, + details: 'No MCP profiles requested for Codex registration.' + }; + } else { + try { + const registeredNames: string[] = []; + for (const profileName of profileNames) { + const registration = (deps.getCodexRegistration ?? serviceManager.codexRegistration.bind(serviceManager))(profileName); + if (!registration) { + continue; + } + + const remove = await spawnCommand('codex', ['mcp', 'remove', registration.codexName], { + cwd, + encoding: 'utf8' + }); + + if (remove.status !== 0 && process.env.CODEX_DEBUG === '1') { + const removeMessage = remove.stderr?.trim() || remove.stdout?.trim() || 'unknown remove failure'; + process.stderr.write( + `[launch] codex mcp remove ${registration.codexName} returned non-zero: ${removeMessage}\n` + ); + } + + const add = await spawnCommand('codex', ['mcp', 'add', registration.codexName, '--url', registration.url], { + cwd, + encoding: 'utf8' + }); + + if (add.status !== 0) { + const stderr = add.stderr?.trim() || ''; + if (/already exists/i.test(stderr)) { + registeredNames.push(registration.codexName); + continue; + } + + throw new BridgeError( + ErrorCode.MCP_ERROR, + `codex mcp add failed for ${registration.codexName}: ${stderr || add.stdout?.trim() || 'unknown error'}`, + { + registration: registration.codexName, + stderr, + stdout: add.stdout + } + ); + } + + registeredNames.push(registration.codexName); + } + + codexRegisterStep = { + id: 'mcp.codex_register', + ok: true, + details: registeredNames.length + ? `Ensured Codex MCP registration for ${registeredNames.join(', ')}` + : 'Selected MCP profiles do not expose Codex registration metadata.' + }; + } catch (error) { + const bridgeError = error instanceof BridgeError ? error : null; + codexRegisterStep = { + id: 'mcp.codex_register', + ok: false, + details: `Failed to register MCP profile(s) with Codex: ${(error as Error).message}`, + remediation: `codex-synaptic env codex-register ${profileNames.join(' ')} --replace`, + metadata: bridgeError + ? { + code: bridgeError.code, + context: bridgeError.context + } + : undefined + }; + } + } + + { + const stop = appendStep(codexRegisterStep); + if (stop) { + return stop; + } + } + + doctorReport = await executeDoctor( + { + cwd, + mcpProfiles: profileNames, + skipCodexAuth: Boolean(options.skipCodexAuth) + }, + { + fileExists, + spawnCommand, + getServiceStatus: deps.getServiceStatus, + getCodexRegistration: deps.getCodexRegistration, + registriesForProfiles: deps.registriesForProfiles + } + ); + + const doctorRemediations = collectDoctorRemediations(doctorReport); + const doctorStep: LaunchStep = { + id: 'doctor.strict', + ok: doctorReport.ok, + details: doctorReport.ok + ? `Doctor passed (${doctorReport.summary.passed}/${doctorReport.summary.total}).` + : `Doctor reported ${doctorReport.summary.failed} failing check(s).`, + remediation: doctorRemediations.length ? doctorRemediations.join(' && ') : undefined + }; + + { + const stop = appendStep(doctorStep); + if (stop) { + return stop; + } + } + + return buildLaunchReport(steps, doctorReport); +} diff --git a/src/core/errors.ts b/src/core/errors.ts index 2585f2e..ac6ffd3 100644 --- a/src/core/errors.ts +++ b/src/core/errors.ts @@ -7,6 +7,8 @@ export enum ErrorCode { SYSTEM_NOT_INITIALIZED = 'SYSTEM_NOT_INITIALIZED', SYSTEM_SHUTDOWN = 'SYSTEM_SHUTDOWN', SYSTEM_OVERLOAD = 'SYSTEM_OVERLOAD', + LAUNCH_GATE_FAILURE = 'LAUNCH_GATE_FAILURE', + DOCTOR_CHECKS_FAILED = 'DOCTOR_CHECKS_FAILED', // Agent errors AGENT_NOT_FOUND = 'AGENT_NOT_FOUND', @@ -86,6 +88,17 @@ export class SystemError extends CodexSynapticError { } } +export class CliGateError extends CodexSynapticError { + constructor( + code: ErrorCode.LAUNCH_GATE_FAILURE | ErrorCode.DOCTOR_CHECKS_FAILED, + message: string, + context?: Record + ) { + super(code, message, context, false); + this.name = 'CliGateError'; + } +} + export class AgentError extends CodexSynapticError { constructor(code: ErrorCode, message: string, context?: Record, retryable: boolean = true) { super(code, message, context, retryable); diff --git a/src/env/service-manager.ts b/src/env/service-manager.ts index fea107d..b26b2de 100644 --- a/src/env/service-manager.ts +++ b/src/env/service-manager.ts @@ -1,6 +1,7 @@ -import { execSync } from 'child_process'; +import { execFileSync, spawn } from 'child_process'; import { createConnection } from 'net'; import { setTimeout as sleep } from 'timers/promises'; +import { CodexSynapticError, ErrorCode } from '../core/errors.js'; import { Logger } from '../core/logger.js'; export class ServiceManagerError extends Error { @@ -20,6 +21,8 @@ export interface ServiceProfile { composeFile: string; services?: string[]; port?: number; + dockerImages?: string[]; + dockerRegistries?: string[]; requiredEnv?: string[]; codexName?: string; healthcheck?: { @@ -44,6 +47,14 @@ export interface EnsureServiceOptions { allowFilesystemWrite?: boolean; } +interface ComposeCommand { + bin: string; + args: string[]; +} + +/** Maximum time (ms) to wait for `docker compose up -d` before killing the process. */ +const COMPOSE_UP_TIMEOUT_MS = 300_000; // 5 minutes + const PROFILES: Record = { observability: { description: 'Prometheus/Grafana stack with exporters', @@ -83,6 +94,7 @@ const PROFILES: Record = { composeFile: 'docker/mcp/docker-compose.github.yml', services: ['mcp-github'], port: 7010, + dockerImages: ['ghcr.io/context-labs/github-mcp:v1.0.0'], requiredEnv: ['GITHUB_TOKEN'], codexName: 'github' }, @@ -91,6 +103,7 @@ const PROFILES: Record = { composeFile: 'docker/mcp/docker-compose.context7.yml', services: ['mcp-context7'], port: 7020, + dockerImages: ['ghcr.io/context-labs/context7-mcp:v1.0.0'], requiredEnv: ['CONTEXT7_API_KEY'], codexName: 'context7' }, @@ -99,6 +112,7 @@ const PROFILES: Record = { composeFile: 'docker/mcp/docker-compose.playwright.yml', services: ['mcp-playwright'], port: 7030, + dockerImages: ['mcp/playwright:v1.0.0'], codexName: 'playwright-local' }, 'mcp-filesystem': { @@ -106,6 +120,7 @@ const PROFILES: Record = { composeFile: 'docker/mcp/docker-compose.filesystem.yml', services: ['mcp-filesystem'], port: 7040, + dockerImages: ['ghcr.io/context-labs/filesystem-mcp:v1.0.0'], codexName: 'filesystem-local' }, 'mcp-desktop-commander': { @@ -113,6 +128,7 @@ const PROFILES: Record = { composeFile: 'docker/mcp/docker-compose.desktop-commander.yml', services: ['mcp-desktop-commander'], port: 7070, + dockerImages: ['ghcr.io/wonderwhy-er/desktop-commander:v1.0.0'], codexName: 'desktop-commander' }, 'mcp-tavily': { @@ -120,6 +136,7 @@ const PROFILES: Record = { composeFile: 'docker/mcp/docker-compose.tavily.yml', services: ['mcp-tavily'], port: 7050, + dockerImages: ['ghcr.io/context-labs/tavily-mcp:v1.0.0'], requiredEnv: ['TAVILY_API_KEY'], codexName: 'tavily' }, @@ -128,14 +145,26 @@ const PROFILES: Record = { composeFile: 'docker/mcp/docker-compose.firecrawl.yml', services: ['mcp-firecrawl'], port: 7060, + dockerImages: ['ghcr.io/firecrawl/firecrawl-mcp:v1.0.0'], requiredEnv: ['FIRECRAWL_API_KEY'], codexName: 'firecrawl' } }; -function composeCommand(profile: ServiceProfile, command: string, services?: string[]): string { - const serviceArgs = services && services.length ? ` ${services.join(' ')}` : ''; - return `docker compose -f ${profile.composeFile} ${command}${serviceArgs}`; +/** + * Builds a structured {@link ComposeCommand} for the given profile and compose sub-command. + * + * @param command - A simple space-separated compose command string (e.g. `'up -d'`, `'down'`, `'ps'`). + * Arguments containing spaces or special characters are not supported; all current callers pass + * literal, shell-safe strings. + */ +function composeCommand(profile: ServiceProfile, command: string, services?: string[]): ComposeCommand { + const cmdArgs = command.trim().split(/\s+/); + const serviceArgs = services && services.length ? services : []; + return { + bin: 'docker', + args: ['compose', '-f', profile.composeFile, ...cmdArgs, ...serviceArgs] + }; } class ServiceManager { @@ -190,11 +219,16 @@ class ServiceManager { async ensureService(name: string, options?: EnsureServiceOptions): Promise { const profile = this.getProfile(name); - const cmd = composeCommand(profile, 'up -d', profile.services); + const composed = composeCommand(profile, 'up -d', profile.services); + const cmdString = [composed.bin, ...composed.args].join(' '); const env = this.resolveExecEnv(name, options); - this.logger.info('env', `Starting service ${name}`, { command: cmd }); - execSync(cmd, { stdio: 'inherit', env }); + this.logger.info('env', `Starting service ${name}`, { command: cmdString }); + try { + await this.runComposeUp(composed, env); + } catch (error) { + throw this.wrapComposeStartError(name, profile, cmdString, error); + } if (options?.waitForHealth !== false) { await this.waitForServiceHealth(name, profile); @@ -203,14 +237,14 @@ class ServiceManager { stopService(name: string): void { const profile = this.getProfile(name); - const cmd = composeCommand(profile, 'down', profile.services); - this.logger.info('env', `Stopping service ${name}`, { command: cmd }); - execSync(cmd, { stdio: 'inherit' }); + const { bin, args } = composeCommand(profile, 'down', profile.services); + this.logger.info('env', `Stopping service ${name}`, { command: [bin, ...args].join(' ') }); + execFileSync(bin, args, { stdio: 'inherit' }); } async status(name: string): Promise { const profile = this.getProfile(name); - const cmd = composeCommand(profile, 'ps'); + const { bin, args } = composeCommand(profile, 'ps'); const diagnostics: string[] = []; for (const required of profile.requiredEnv ?? []) { @@ -220,7 +254,7 @@ class ServiceManager { } try { - const output = execSync(cmd, { stdio: 'pipe' }).toString(); + const output = execFileSync(bin, args, { stdio: 'pipe' }).toString(); const running = /\bUp\b/.test(output); if (!running) { @@ -279,6 +313,222 @@ class ServiceManager { }; } + dockerImagesForProfiles(names: string[]): string[] { + const images = new Set(); + + for (const name of names) { + const profile = this.getProfile(name); + for (const image of profile.dockerImages ?? []) { + const normalized = image.trim(); + if (normalized) { + images.add(normalized); + } + } + } + + return Array.from(images); + } + + registriesForProfiles(names: string[]): string[] { + const registries = new Set(); + + for (const name of names) { + const profile = this.getProfile(name); + + for (const registry of profile.dockerRegistries ?? []) { + const normalized = registry.trim(); + if (normalized) { + registries.add(normalized); + } + } + + for (const image of profile.dockerImages ?? []) { + const registry = this.registryForImage(image); + if (registry) { + registries.add(registry); + } + } + } + + return Array.from(registries); + } + + async dockerLogin(registry: string): Promise { + const normalized = registry.trim(); + if (!normalized) { + throw new Error('Docker registry is required for docker login.'); + } + this.logger.info('env', 'Authenticating Docker registry', { registry: normalized }); + await new Promise((resolve, reject) => { + const child = spawn('docker', ['login', normalized], { stdio: 'inherit' }); + child.on('error', (error) => { + reject(error); + }); + child.on('close', (code) => { + if (code === 0) { + resolve(); + return; + } + reject(new Error(`docker login exited with status ${code ?? 'unknown'}`)); + }); + }); + } + + private registryForImage(image: string): string | null { + const normalized = image.trim(); + if (!normalized) { + return null; + } + + const firstSegment = normalized.split('/')[0] ?? ''; + if (!firstSegment) { + return null; + } + + // Registry host is explicit only when the first segment contains host-like syntax. + if ( + firstSegment.includes('.') + || firstSegment.includes(':') + || firstSegment === 'localhost' + ) { + return firstSegment; + } + + return null; + } + + /** + * Runs `docker compose up -d` streaming stdout to the terminal (avoiding + * ENOBUFS on large image pulls) while capturing stderr for error + * classification by {@link wrapComposeStartError}. + * + * A process-level timeout of {@link COMPOSE_UP_TIMEOUT_MS} is applied: if + * the child process does not exit within that window it is killed and the + * returned Promise is rejected with a descriptive timeout error. + */ + private runComposeUp(composed: ComposeCommand, env: NodeJS.ProcessEnv): Promise { + return new Promise((resolve, reject) => { + const proc = spawn(composed.bin, composed.args, { shell: false, stdio: ['ignore', 'inherit', 'pipe'], env }); + const stderrChunks: Buffer[] = []; + let settled = false; + + const settle = (fn: () => void): void => { + if (settled) return; + settled = true; + clearTimeout(timer); + fn(); + }; + + const timer = setTimeout(() => { + proc.kill(); + settle(() => { + reject( + Object.assign( + new Error(`docker compose timed out after ${COMPOSE_UP_TIMEOUT_MS}ms`), + { status: null, stdout: '', stderr: `Process timed out after ${COMPOSE_UP_TIMEOUT_MS}ms` }, + ), + ); + }); + }, COMPOSE_UP_TIMEOUT_MS); + + proc.stderr?.on('data', (chunk: Buffer) => stderrChunks.push(chunk)); + + proc.on('close', (code) => { + settle(() => { + if (code === 0) { + resolve(); + } else { + const stderr = Buffer.concat(stderrChunks).toString('utf8'); + reject( + Object.assign(new Error(`docker compose exited with code ${code}`), { + status: code, + stdout: '', + stderr, + }), + ); + } + }); + }); + + proc.on('error', (spawnErr) => { + settle(() => { + reject( + Object.assign(spawnErr, { + status: null, + stdout: '', + stderr: spawnErr.message, + }), + ); + }); + }); + }); + } + + private wrapComposeStartError(name: string, profile: ServiceProfile, cmd: string, error: unknown): Error { + const execError = error as { + status?: number | null; + stdout?: string | Buffer; + stderr?: string | Buffer; + message?: string; + }; + + const stdout = this.toText(execError.stdout); + const stderr = this.toText(execError.stderr); + const combined = [stderr, stdout] + .filter(Boolean) + .join('\n') + .trim(); + const output = combined || (execError.message?.trim() ?? 'Unknown docker compose error'); + const exitStatus = typeof execError.status === 'number' ? execError.status : null; + const images = profile.dockerImages?.join(', ') || 'unknown image'; + + let diagnosis = `Docker compose startup failed for ${name}`; + let remediation = 'Verify Docker is running, then retry.'; + + if (/pull access denied|requested access to the resource is denied|insufficient_scope|unauthorized|authentication required|error from registry:\s*denied/i.test(output)) { + diagnosis = `Docker image pull/auth denied for ${name} (${images})`; + remediation = `Run \`codex-synaptic env docker-login ${name}\` and retry \`codex-synaptic env up ${name}\`.`; + } else if (/Cannot connect to the Docker daemon|Is the docker daemon running/i.test(output)) { + diagnosis = `Docker daemon unavailable while starting ${name}`; + remediation = 'Start Docker Desktop (or the Docker daemon) and retry.'; + } else if (/command not found|ENOENT/i.test(output)) { + diagnosis = `Docker CLI unavailable while starting ${name}`; + remediation = 'Install Docker with the Compose plugin and ensure `docker compose` works.'; + } + + const truncatedOutput = output.length > 500 ? `${output.slice(0, 500)}…` : output; + const exitLabel = exitStatus === null ? 'unknown' : String(exitStatus); + + return new CodexSynapticError( + ErrorCode.BRIDGE_ERROR, + `${diagnosis} (exit=${exitLabel}, compose=${cmd}). ${remediation} Raw docker output: ${truncatedOutput}`, + { + code: 'COMPOSE_START_FAILED', + diagnosis, + remediation, + exitStatus, + composeCmd: cmd, + output: truncatedOutput, + images, + serviceName: name, + profile: profile.composeFile + }, + false + ); + } + + private toText(value: unknown): string { + if (typeof value === 'string') { + return value.trim(); + } + + if (Buffer.isBuffer(value)) { + return value.toString('utf8').trim(); + } + + return ''; + } + private async probeService(profile: ServiceProfile): Promise { if (profile.healthcheck?.url) { return this.probeHttp(profile.healthcheck.url, 2000); diff --git a/tests/cli/doctor.test.ts b/tests/cli/doctor.test.ts new file mode 100644 index 0000000..afc9408 --- /dev/null +++ b/tests/cli/doctor.test.ts @@ -0,0 +1,218 @@ +import { describe, expect, it } from 'vitest'; +import { runDoctor, type DoctorDependencies } from '../../src/cli/doctor'; +import type { ServiceStatus } from '../../src/env/service-manager'; + +function serviceStatus(overrides: Partial = {}): ServiceStatus { + return { + name: 'mcp-filesystem', + running: true, + healthy: true, + raw: 'ok', + diagnostics: [], + checkedAt: '2026-02-14T00:00:00.000Z', + ...overrides + }; +} + +describe('runDoctor', () => { + it('fails when the dist CLI artifact is missing', async () => { + const deps: DoctorDependencies = { + fileExists: () => false, + spawnCommand: async (command, args) => { + if (command === 'codex' && args.join(' ') === 'mcp list --json') { + return { + status: 0, + stdout: '[{"name":"filesystem-local"}]', + stderr: '' + }; + } + throw new Error(`Unexpected command: ${command} ${args.join(' ')}`); + }, + getServiceStatus: async () => serviceStatus(), + getCodexRegistration: () => ({ codexName: 'filesystem-local', url: 'http://localhost:7040' }) + }; + + const report = await runDoctor( + { + cwd: '/tmp/codex-synaptic', + skipCodexAuth: true, + mcpProfiles: ['mcp-filesystem'] + }, + deps + ); + + expect(report.ok).toBe(false); + expect(report.summary.failed).toBe(1); + expect(report.checks.find((check) => check.id === 'repo.cli_build_artifact')?.ok).toBe(false); + }); + + it('fails codex auth check when codex login status returns non-zero', async () => { + const deps: DoctorDependencies = { + fileExists: () => true, + spawnCommand: async (command, args) => { + if (command === 'node' && args.includes('--help')) { + return { status: 0, stdout: 'ok', stderr: '' }; + } + + if (command === 'codex' && args.join(' ') === 'login status') { + return { status: 1, stdout: '', stderr: 'Not logged in' }; + } + + if (command === 'codex' && args.join(' ') === 'mcp list --json') { + return { + status: 0, + stdout: '[{"name":"filesystem-local"}]', + stderr: '' + }; + } + + throw new Error(`Unexpected command: ${command} ${args.join(' ')}`); + }, + getServiceStatus: async () => serviceStatus(), + getCodexRegistration: () => ({ codexName: 'filesystem-local', url: 'http://localhost:7040' }) + }; + + const report = await runDoctor( + { + cwd: '/tmp/codex-synaptic', + mcpProfiles: ['mcp-filesystem'] + }, + deps + ); + + const authCheck = report.checks.find((check) => check.id === 'codex.auth'); + expect(authCheck?.ok).toBe(false); + expect(authCheck?.details).toContain('Not logged in'); + expect(authCheck?.remediation).toContain('codex login'); + expect(report.ok).toBe(false); + }); + + it('passes all checks when auth, MCP registration, and services are healthy', async () => { + const profileRegistrations: Record = { + 'mcp-filesystem': { codexName: 'filesystem-local', url: 'http://localhost:7040' }, + 'mcp-playwright': { codexName: 'playwright-local', url: 'http://localhost:7030' } + }; + + const deps: DoctorDependencies = { + fileExists: () => true, + spawnCommand: async (command, args) => { + if (command === 'node' && args.includes('--help')) { + return { status: 0, stdout: 'ok', stderr: '' }; + } + + if (command === 'codex' && args.join(' ') === 'login status') { + return { status: 0, stdout: 'Logged in as test-user', stderr: '' }; + } + + if (command === 'codex' && args.join(' ') === 'mcp list --json') { + return { + status: 0, + stdout: JSON.stringify([ + { name: 'filesystem-local' }, + { name: 'playwright-local' } + ]), + stderr: '' + }; + } + + throw new Error(`Unexpected command: ${command} ${args.join(' ')}`); + }, + getServiceStatus: async (name) => serviceStatus({ name }), + getCodexRegistration: (name) => profileRegistrations[name] ?? null + }; + + const report = await runDoctor( + { + cwd: '/tmp/codex-synaptic', + mcpProfiles: ['mcp-filesystem', 'mcp-playwright'] + }, + deps + ); + + expect(report.ok).toBe(true); + expect(report.summary.failed).toBe(0); + expect(report.checks.find((check) => check.id === 'repo.cli_exec')?.ok).toBe(true); + expect(report.checks.find((check) => check.id === 'mcp.mcp-filesystem')?.ok).toBe(true); + expect(report.checks.find((check) => check.id === 'mcp.mcp-playwright')?.ok).toBe(true); + }); + + it('returns actionable remediation for failing MCP profile checks', async () => { + const deps: DoctorDependencies = { + fileExists: () => true, + spawnCommand: async (command, args) => { + if (command === 'node' && args.includes('--help')) { + return { status: 0, stdout: 'ok', stderr: '' }; + } + + if (command === 'codex' && args.join(' ') === 'mcp list --json') { + return { + status: 0, + stdout: '[]', + stderr: '' + }; + } + + throw new Error(`Unexpected command: ${command} ${args.join(' ')}`); + }, + getServiceStatus: async () => serviceStatus({ running: false, healthy: false }), + getCodexRegistration: () => ({ codexName: 'filesystem-local', url: 'http://localhost:7040' }), + registriesForProfiles: () => ['ghcr.io'] + }; + + const report = await runDoctor( + { + cwd: '/tmp/codex-synaptic', + skipCodexAuth: true, + mcpProfiles: ['mcp-filesystem'] + }, + deps + ); + + const mcpCheck = report.checks.find((check) => check.id === 'mcp.mcp-filesystem'); + expect(mcpCheck?.ok).toBe(false); + expect(mcpCheck?.remediation).toContain('codex-synaptic env docker-login mcp-filesystem'); + expect(mcpCheck?.remediation).toContain('codex-synaptic env up mcp-filesystem'); + expect(mcpCheck?.remediation).toContain('codex-synaptic env codex-register mcp-filesystem'); + }); + + it('fails codex MCP parsing checks when codex mcp list returns malformed JSON', async () => { + const deps: DoctorDependencies = { + fileExists: () => true, + spawnCommand: async (command, args) => { + if (command === 'node' && args.includes('--help')) { + return { status: 0, stdout: 'ok', stderr: '' }; + } + + if (command === 'codex' && args.join(' ') === 'mcp list --json') { + return { + status: 0, + stdout: 'not-json', + stderr: '' + }; + } + + throw new Error(`Unexpected command: ${command} ${args.join(' ')}`); + }, + getServiceStatus: async () => serviceStatus(), + getCodexRegistration: () => ({ codexName: 'filesystem-local', url: 'http://localhost:7040' }) + }; + + const report = await runDoctor( + { + cwd: '/tmp/codex-synaptic', + skipCodexAuth: true, + mcpProfiles: ['mcp-filesystem'] + }, + deps + ); + + const mcpListCheck = report.checks.find((check) => check.id === 'codex.mcp_list'); + expect(mcpListCheck?.ok).toBe(false); + expect(mcpListCheck?.remediation).toContain('codex mcp list --json'); + + const mcpProfileCheck = report.checks.find((check) => check.id === 'mcp.mcp-filesystem'); + expect(mcpProfileCheck?.ok).toBe(false); + expect(mcpProfileCheck?.remediation).toContain('codex-synaptic env codex-register mcp-filesystem'); + expect(report.ok).toBe(false); + }); +}); diff --git a/tests/cli/env-bootstrap.test.ts b/tests/cli/env-bootstrap.test.ts new file mode 100644 index 0000000..4e48ab2 --- /dev/null +++ b/tests/cli/env-bootstrap.test.ts @@ -0,0 +1,109 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "fs"; +import { join } from "path"; +import { tmpdir } from "os"; +import { describe, expect, it, onTestFinished } from "vitest"; +import { + bootstrapCliEnv, + buildCliEnvBootstrapMessages, + loadEnvFile, + shouldAutoLoadCliEnv, + shouldShowCliEnvBanner, +} from "../../src/cli/env-bootstrap"; + +describe("cli env bootstrap helper", () => { + it("does not override existing environment variables", async () => { + const tempDir = mkdtempSync(join(tmpdir(), "codex-env-bootstrap-")); + onTestFinished(() => rmSync(tempDir, { recursive: true, force: true })); + const envPath = join(tempDir, ".env"); + writeFileSync(envPath, "EXISTING_KEY=from-file\nNEW_KEY=from-file\n"); + + const env: NodeJS.ProcessEnv = { + EXISTING_KEY: "from-env", + }; + + const applied = await loadEnvFile(envPath, env); + expect(applied).toBe(true); + expect(env.EXISTING_KEY).toBe("from-env"); + expect(env.NEW_KEY).toBe("from-file"); + }); + + it("autoload is enabled by default and can be disabled explicitly", () => { + expect(shouldAutoLoadCliEnv({})).toBe(true); + expect(shouldAutoLoadCliEnv({ CODEX_CLI_ENV_AUTOLOAD: "0" })).toBe(false); + expect(shouldAutoLoadCliEnv({ CODEX_CLI_ENV_AUTOLOAD: "false" })).toBe( + false, + ); + expect(shouldAutoLoadCliEnv({ CODEX_CLI_ENV_AUTOLOAD: "1" })).toBe(true); + }); + + it("suppresses env banner in json mode unless forced", () => { + expect( + shouldShowCliEnvBanner({ + env: {}, + cliSilent: false, + argv: ["node", "dist/cli/index.js", "doctor", "--json"], + }), + ).toBe(false); + + expect( + shouldShowCliEnvBanner({ + env: { CODEX_CLI_ENV_BANNER_FORCE: "1" }, + cliSilent: false, + argv: ["node", "dist/cli/index.js", "doctor", "--json"], + }), + ).toBe(true); + }); + + it("builds sanitized banner messages by default and verbose paths on opt-in", () => { + const cwd = "/repo"; + const sources = ["/repo/.env.local", "/repo/src/cli/.env"]; + + const defaultMessages = buildCliEnvBootstrapMessages(sources, { + cwd, + env: {}, + }); + expect(defaultMessages[0]).toContain("local .env file(s)"); + expect(defaultMessages[0]).toContain("(2)"); + expect(defaultMessages[0]).not.toContain("src/cli/.env"); + expect(defaultMessages.some((line) => line.includes("src/cli/.env"))).toBe( + true, + ); + expect( + defaultMessages.some((line) => line.includes("CODEX_CLI_ENV_AUTOLOAD=0")), + ).toBe(true); + + const verboseMessages = buildCliEnvBootstrapMessages(sources, { + cwd, + env: { CODEX_CLI_ENV_BANNER_VERBOSE: "1" }, + }); + expect(verboseMessages[0]).toContain(".env.local"); + expect(verboseMessages[0]).toContain("src/cli/.env"); + }); + + it("bootstraps candidate env files in precedence order without duplicates", async () => { + const tempDir = mkdtempSync(join(tmpdir(), "codex-env-bootstrap-")); + onTestFinished(() => rmSync(tempDir, { recursive: true, force: true })); + writeFileSync(join(tempDir, ".env"), "BASE_KEY=base\n"); + writeFileSync( + join(tempDir, ".env.local"), + "BASE_KEY=local-override-ignored\nLOCAL_KEY=local\n", + ); + const srcCliDir = join(tempDir, "src", "cli"); + mkdirSync(srcCliDir, { recursive: true }); + writeFileSync(join(tempDir, "src", "cli", ".env"), "CLI_KEY=cli\n", { + flag: "w", + }); + + const env: NodeJS.ProcessEnv = {}; + const sources = await bootstrapCliEnv({ cwd: tempDir, env }); + + expect(sources.map((source) => source.replace(`${tempDir}/`, ""))).toEqual([ + ".env", + ".env.local", + "src/cli/.env", + ]); + expect(env.BASE_KEY).toBe("base"); + expect(env.LOCAL_KEY).toBe("local"); + expect(env.CLI_KEY).toBe("cli"); + }); +}); diff --git a/tests/cli/launch.test.ts b/tests/cli/launch.test.ts new file mode 100644 index 0000000..c2cb86d --- /dev/null +++ b/tests/cli/launch.test.ts @@ -0,0 +1,233 @@ +import { describe, expect, it, vi } from 'vitest'; +import { LaunchNextAction, runLaunch, type LaunchDependencies } from '../../src/cli/launch'; +import type { DoctorReport } from '../../src/cli/doctor'; +import { Logger } from '../../src/core/logger'; + +const passingDoctorReport: DoctorReport = { + ok: true, + summary: { passed: 0, failed: 0, total: 0 }, + checks: [] +}; + +describe('runLaunch', () => { + it('returns ready=true when all launch gates pass', async () => { + const ensuredProfiles: string[] = []; + const spawnCalls: string[] = []; + + const deps: LaunchDependencies = { + fileExists: () => true, + spawnCommand: async (command, args) => { + spawnCalls.push(`${command} ${args.join(' ')}`); + + if (command === 'node' && args.includes('--help')) { + return { status: 0, stdout: 'ok', stderr: '' }; + } + + if (command === 'codex' && args.join(' ') === 'login status') { + return { status: 0, stdout: 'Logged in as test-user', stderr: '' }; + } + + if (command === 'codex' && args[0] === 'mcp' && args[1] === 'remove') { + return { status: 0, stdout: '', stderr: '' }; + } + + if (command === 'codex' && args[0] === 'mcp' && args[1] === 'add') { + return { status: 0, stdout: '', stderr: '' }; + } + + throw new Error(`Unexpected command: ${command} ${args.join(' ')}`); + }, + getBackgroundStatus: () => ({ running: false }), + startBackground: async () => ({ running: true, pid: 43210 }), + ensureService: async (name) => { + ensuredProfiles.push(name); + }, + getCodexRegistration: (name) => { + if (name === 'mcp-filesystem') { + return { codexName: 'filesystem-local', url: 'http://localhost:7040' }; + } + if (name === 'mcp-playwright') { + return { codexName: 'playwright-local', url: 'http://localhost:7030' }; + } + return null; + }, + runDoctor: async () => passingDoctorReport + }; + + const report = await runLaunch( + { + cwd: '/tmp/codex-synaptic', + strict: true, + mcpProfiles: ['mcp-filesystem', 'mcp-playwright'] + }, + deps + ); + + expect(report.ok).toBe(true); + expect(report.nextAction).toBe(LaunchNextAction.Continue); + expect(report.steps.map((step) => step.id)).toEqual([ + 'repo.preflight', + 'codex.auth', + 'runtime.daemon', + 'mcp.up', + 'mcp.codex_register', + 'doctor.strict' + ]); + expect(ensuredProfiles).toEqual(['mcp-filesystem', 'mcp-playwright']); + expect(spawnCalls).toContain('codex mcp add filesystem-local --url http://localhost:7040'); + expect(spawnCalls).toContain('codex mcp add playwright-local --url http://localhost:7030'); + }); + + it('fail-fast stops immediately on the first failing gate in strict mode', async () => { + let doctorCalled = false; + + const report = await runLaunch( + { + cwd: '/tmp/codex-synaptic', + strict: true, + mcpProfiles: ['mcp-filesystem'] + }, + { + fileExists: () => false, + runDoctor: async () => { + doctorCalled = true; + return passingDoctorReport; + } + } + ); + + expect(report.ok).toBe(false); + expect(report.nextAction).toBe(LaunchNextAction.Stop); + expect(report.steps).toHaveLength(1); + expect(report.steps[0].id).toBe('repo.preflight'); + expect(report.doctor.summary.total).toBe(0); + expect(doctorCalled).toBe(false); + }); + + it('returns remediation commands when MCP startup fails', async () => { + const report = await runLaunch( + { + cwd: '/tmp/codex-synaptic', + strict: true, + skipCodexAuth: true, + mcpProfiles: ['mcp-filesystem'] + }, + { + fileExists: () => true, + spawnCommand: async (command, args) => { + if (command === 'node' && args.includes('--help')) { + return { status: 0, stdout: 'ok', stderr: '' }; + } + throw new Error(`Unexpected command: ${command} ${args.join(' ')}`); + }, + getBackgroundStatus: () => ({ running: true, pid: 999 }), + registriesForProfiles: () => ['ghcr.io'], + ensureService: async () => { + throw new Error('docker compose timeout'); + }, + runDoctor: async () => passingDoctorReport + } + ); + + expect(report.ok).toBe(false); + expect(report.nextAction).toBe(LaunchNextAction.Stop); + const mcpStep = report.steps.find((step) => step.id === 'mcp.up'); + expect(mcpStep?.ok).toBe(false); + expect(mcpStep?.details).toContain('mcp-filesystem'); + expect(mcpStep?.details).toContain('after starting 0/1'); + expect(mcpStep?.remediation).toContain('codex-synaptic env docker-login mcp-filesystem'); + expect(mcpStep?.remediation).toContain('codex-synaptic env up mcp-filesystem'); + expect(mcpStep?.remediation).toContain('codex-synaptic env codex-register mcp-filesystem --replace'); + expect(mcpStep?.remediation).toContain('codex-synaptic env status mcp-filesystem'); + expect((mcpStep?.metadata as { failedProfile?: string } | undefined)?.failedProfile).toBe('mcp-filesystem'); + }); + + it('captures MCP bridge error classification when codex registration add fails', async () => { + let doctorCalled = false; + const report = await runLaunch( + { + cwd: '/tmp/codex-synaptic', + strict: true, + skipCodexAuth: true, + mcpProfiles: ['mcp-filesystem'] + }, + { + fileExists: () => true, + spawnCommand: async (command, args) => { + if (command === 'node' && args.includes('--help')) { + return { status: 0, stdout: 'ok', stderr: '' }; + } + if (command === 'codex' && args[0] === 'mcp' && args[1] === 'remove') { + return { status: 0, stdout: '', stderr: '' }; + } + if (command === 'codex' && args[0] === 'mcp' && args[1] === 'add') { + return { status: 1, stdout: 'denied', stderr: 'permission denied' }; + } + throw new Error(`Unexpected command: ${command} ${args.join(' ')}`); + }, + getBackgroundStatus: () => ({ running: true, pid: 999 }), + ensureService: async () => {}, + getCodexRegistration: () => ({ codexName: 'filesystem-local', url: 'http://localhost:7040' }), + runDoctor: async () => { + doctorCalled = true; + return passingDoctorReport; + } + } + ); + + expect(report.ok).toBe(false); + expect(report.nextAction).toBe(LaunchNextAction.Stop); + expect(doctorCalled).toBe(false); + const registrationStep = report.steps.find((step) => step.id === 'mcp.codex_register'); + expect(registrationStep?.ok).toBe(false); + expect(registrationStep?.details).toContain('codex mcp add failed for filesystem-local'); + expect(registrationStep?.remediation).toContain('codex-synaptic env codex-register mcp-filesystem --replace'); + expect((registrationStep?.metadata as { code?: string } | undefined)?.code).toBe('MCP_ERROR'); + }); + + it('suppresses info-level console logs during MCP startup when configured', async () => { + const logger = Logger.getInstance(); + const previousConsoleLevel = logger.getConsoleLevel(); + const infoSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); + + try { + const report = await runLaunch( + { + cwd: '/tmp/codex-synaptic', + strict: true, + skipCodexAuth: true, + mcpProfiles: ['mcp-filesystem'], + suppressInfoConsoleLogs: true + }, + { + fileExists: () => true, + spawnCommand: async (command, args) => { + if (command === 'node' && args.includes('--help')) { + return { status: 0, stdout: 'ok', stderr: '' }; + } + if (command === 'codex' && args[0] === 'mcp' && args[1] === 'remove') { + return { status: 0, stdout: '', stderr: '' }; + } + if (command === 'codex' && args[0] === 'mcp' && args[1] === 'add') { + return { status: 0, stdout: '', stderr: '' }; + } + throw new Error(`Unexpected command: ${command} ${args.join(' ')}`); + }, + getBackgroundStatus: () => ({ running: true, pid: 999 }), + ensureService: async (name) => { + logger.info('env', `Starting service ${name}`, { command: 'docker compose up -d' }); + }, + getCodexRegistration: () => ({ codexName: 'filesystem-local', url: 'http://localhost:7040' }), + runDoctor: async () => passingDoctorReport + } + ); + + expect(report.ok).toBe(true); + expect(infoSpy).not.toHaveBeenCalled(); + expect(logger.getConsoleLevel()).toBe(previousConsoleLevel); + } finally { + infoSpy.mockRestore(); + logger.setConsoleLevel(previousConsoleLevel); + } + }); +}); diff --git a/tests/e2e/cli-smoke.test.ts b/tests/e2e/cli-smoke.test.ts index 502c36e..808aa0c 100644 --- a/tests/e2e/cli-smoke.test.ts +++ b/tests/e2e/cli-smoke.test.ts @@ -127,12 +127,24 @@ describe('codex-synaptic CLI smoke suite', () => { expect(stdout).toContain('repo.cli_build_artifact'); }); + it('exposes launch command gating options', () => { + const { stdout } = runCli(['launch', '--help']); + expect(stdout).toContain('--mcp-profiles'); + expect(stdout).toContain('--no-strict'); + expect(stdout).toContain('--skip-codex-auth'); + }); + it('includes desktop commander profile in env planning', () => { const { stdout } = runCli(['env', 'plan', 'mcp-desktop-commander']); expect(stdout).toContain('mcp-desktop-commander'); expect(stdout).toContain('codex mcp name'); }); + it('exposes docker-login helper under env command surface', () => { + const { stdout } = runCli(['env', '--help']); + expect(stdout).toContain('docker-login'); + }); + it('exposes the tui command surface', () => { const { stdout } = runCli(['tui', '--help']); expect(stdout).toContain('attach-daemon'); diff --git a/tests/env/service-manager.test.ts b/tests/env/service-manager.test.ts index a0bd65d..445aafe 100644 --- a/tests/env/service-manager.test.ts +++ b/tests/env/service-manager.test.ts @@ -31,4 +31,13 @@ describe('serviceManager profiles', () => { url: 'http://localhost:7040' }); }); + + it('derives docker registries for MCP profiles', () => { + const registries = serviceManager.registriesForProfiles([ + 'mcp-filesystem', + 'mcp-playwright', + 'mcp-desktop-commander' + ]); + expect(registries).toContain('ghcr.io'); + }); });