Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,21 @@ FROM python:3.14-slim
# downloads, and serves from Docker installs.
# git/cmake are required when Cookbook builds llama.cpp on first llama.cpp
# launch inside Docker.
# nodejs/npm provide npx for the built-in Browser MCP server.
# chromium provides the actual browser binary used by that MCP server.
# nodejs/npm provide npx for the optional built-in Browser MCP server.
# Google Chrome is its required browser channel; the MCP's headless mode still
# launches this installed browser binary as the non-root app user.
# gosu lets the entrypoint drop privileges cleanly so signals still reach
# uvicorn directly (no extra shell layer like `su`/`sudo` would add).
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
cmake \
curl \
git \
gnupg \
nodejs \
npm \
chromium \
ripgrep \
tmux \
openssh-client \
gosu \
Expand All @@ -37,6 +40,20 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libmagic1 \
&& rm -rf /var/lib/apt/lists/*

# Install the Google Chrome channel that @playwright/mcp uses by default. Keep
# the repository key isolated in /etc/apt/keyrings and install before the app
# source so browser provisioning remains a reusable image layer. This is a
# known-good Debian package lock, not the moving google-chrome-stable head.
ARG GOOGLE_CHROME_VERSION=151.0.7922.108-1
RUN install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://dl.google.com/linux/linux_signing_key.pub \
| gpg --dearmor -o /etc/apt/keyrings/google-chrome.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/google-chrome.gpg] http://dl.google.com/linux/chrome/deb/ stable main" \
> /etc/apt/sources.list.d/google-chrome.list \
&& apt-get update \
&& apt-get install -y --no-install-recommends google-chrome-stable=${GOOGLE_CHROME_VERSION} \
&& rm -rf /var/lib/apt/lists/*

# libgl1/libglib2.0-0t64/libxcb1 are runtime shared libs (libGL.so.1,
# libglib-2.0/libgthread, libxcb.so.1) that opencv-python (cv2) loads. The
# slim base omits them, so the Cookbook "install realesrgan" path imports cv2
Expand Down Expand Up @@ -92,7 +109,27 @@ COPY --from=realesrgan-wheels /wheels/ /tmp/odysseus-wheels/
RUN pip install --no-cache-dir --no-deps /tmp/odysseus-wheels/*.whl \
&& rm -rf /tmp/odysseus-wheels

# Copy app code
# Prewarm Browser MCP before copying application sources. This preserves the
# existing pinned browser layer when application or LSP runtime code changes,
# so an LSP-only rebuild does not fetch an unrelated package.
RUN groupadd --gid 1000 odysseus \
&& useradd --uid 1000 --gid 1000 --home-dir /app --no-create-home --shell /bin/sh odysseus \
&& mkdir -p /app/.npm /app/.cache/ms-playwright /app/.cache/ms-playwright-mcp \
&& chown -R odysseus:odysseus /app/.npm /app/.cache \
&& gosu odysseus env HOME=/app npm_config_cache=/app/.npm \
npx -y @playwright/mcp@0.0.78 --version

# Pinned offline-only runtime for the optional LSP MCP server. The unified
# upstream server names its backends as @latest, so its process PATH is later
# pointed at the strict local dispatcher below rather than allowing npx to
# resolve packages at runtime.
COPY docker/lsp-runtime/package.json docker/lsp-runtime/package-lock.json /opt/odysseus-lsp/
RUN npm --prefix /opt/odysseus-lsp ci --ignore-scripts --no-audit --no-fund
COPY docker/lsp-runtime/npx /opt/odysseus-lsp/bin/npx
RUN chmod 0755 /opt/odysseus-lsp/bin/npx
COPY docker/lsp-runtime/verify_lsp_runtime.py /opt/odysseus-lsp/verify_lsp_runtime.py

# Copy app code after the pinned runtime layers.
COPY . .

# Create data directory (mount a volume here for persistence)
Expand Down
107 changes: 107 additions & 0 deletions LSP_DOCKER_HANDOFF_20260807_01.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# LSP Docker Activation — Handoff

**Created:** 2026-08-07T16:57:54Z<br>
**Scope:** Dockerized Odysseus LSP MCP only.<br>
**State:** `PARTIALLY_VERIFIED / LIVE_CONNECTION_VERIFIED / SEMANTIC_CLOSEOUT_PENDING`.

## Current live state

| Item | Observed state |
|---|---|
| Odysseus service | Running Docker container `86b02519adf8a487ffe6be7f1d98946ec9d8df7d2979cd199ff686e05b11fb47` |
| Image | `sha256:c2fb085dad45b173c093f8067485b1ff95639ba1fc6ac4c44380c4ea718f0d65` |
| Loopback service | `GET http://127.0.0.1:7000/api/health` returned `{"status":"healthy",...}` at 2026-08-07T16:57:55Z |
| Dashboard | `127.0.0.1:7001` remains loopback-bound; prior check returned its expected `302` authentication redirect |
| LSP record | ID `396e33c2-e4a5-4fae-b60d-b1c1ca5ed0f7`; enabled; Linux container entrypoint `/opt/odysseus-lsp/node_modules/@treedy/lsp-mcp/dist/index.js` |
| Live MCP connection | `data/logs/app.log` recorded `MCP server connected: LSP Code Intelligence (Pinned) ... - 101 tools via stdio` at 2026-08-07T16:54:00Z |
| Live process | `node /opt/odysseus-lsp/node_modules/@treedy/lsp-mcp/dist/index.js` runs as the `odysseus` user under the current container |
| Fixture mount | `tests/fixtures/lsp_docker` is mounted at `/workspace/lsp_fixture` read-only |

## Implemented changes

1. **Reproducible image runtime**
- Added `docker/lsp-runtime/package.json` and lockfile.
- Exact runtime roots: `@treedy/lsp-mcp@0.2.8`, `pyright@1.1.411`, `@treedy/pyright-mcp@1.1.8`, and `@treedy/typescript-lsp-mcp@0.1.7`.
- Docker installs the exact lockfile into `/opt/odysseus-lsp` with `npm ci --ignore-scripts --no-audit --no-fund`.

2. **Runtime network lock**
- Added `docker/lsp-runtime/npx`, which intercepts the upstream LSP package's `@latest` backend requests and dispatches only the pinned Python or TypeScript backend.
- It delegates `npx --version` only and rejects undeclared package requests with exit `64`.
- The live container proved the four installed runtime versions and `SHIM_UNDECLARED_PACKAGE=REJECTED`.

3. **Safe MCP configuration update**
- Added the supported `odysseus-mcp update` command for an existing server's explicit `args`, `env`, and `is_enabled` fields.
- Input JSON is validated; unknown IDs fail closed; output redacts all environment values.
- The existing LSP record was updated using this CLI while Odysseus was stopped, then Odysseus was recreated.

4. **Read-only semantic fixture**
- Added Python and TypeScript fixtures plus `package.json` and `tsconfig.json`.
- Added a single Compose mount: `./tests/fixtures/lsp_docker:/workspace/lsp_fixture:ro,z`.

5. **Build repair**
- The first image rebuild exposed a stale Chrome package pin (`151.0.7922.71-1`) that was no longer present in Google's repository.
- Verified repository metadata supplied `151.0.7922.108-1`; Dockerfile and its focused regression test were updated to that exact version.
- Second build completed successfully.

## Evidence achieved

### Focused source tests

```text
23 passed, 1 warning in 1.02s
```

This included the MCP CLI update tests, Docker hardening tests, MCP dependency pin test, and built-in MCP cache tests.

### Offline container acceptance

A temporary container used the newly built image, mounted only the fixture, and ran with `--network none`.

- Main MCP initialized with `INIT_TOOL_COUNT=101`.
- Python backend started from pinned `pyright-mcp@1.1.8`.
- TypeScript backend started from pinned `typescript-lsp-mcp@0.1.7`.
- Python diagnostics reported the intentional `undefined_symbol` error.
- TypeScript diagnostics reported `Type 'string' is not assignable to type 'number'.`
- No runtime registry resolution was possible because the temporary container had no network.

## Preserved rollback point

| Artifact | SHA-256 | Verification |
|---|---|---|
| `G:\AIW\05_BAK\ODY_LSP_DOCKER_PRE_ACTIVATION_20260807_165042_02\app.db` | `303e42a612dbf04925289974e9c1ae46fcda6063d382f0a73105e56779200567` | SQLite `integrity_check=ok`; LSP record was disabled and retained its original Windows host path |

The earlier `_01` backup directory is empty because its first attempted native-Python destination path used an MSYS `/g/...` path and failed before database output was created. It was preserved; nothing was deleted.

## Remaining closeout work — not executed because the user requested this handoff

1. Rebuild the image once more. The source-only addition that prints semantic tool schemas in `docker/lsp-runtime/verify_lsp_runtime.py` was made after the currently running image was built. It does not change runtime behavior, but source/image identity must be restored before final closeout.
2. Run the finalized offline probe and add the following semantic checks using the now-observed schemas:
- `definition(file, line, column)` for `defined_symbol`;
- `references(file, line, column, page_size)` for `defined_symbol`;
- `rename(file, line, column, newName)` and assert a workspace-edit/dry-run only;
- re-hash both fixture source files afterward.
3. Perform the same semantic calls through the authenticated live Odysseus MCP manager if a currently authenticated Odysseus UI/browser session is available. Direct HTTP access to `/api/mcp/servers` correctly returns `401`; no credentials or authentication settings were read, changed, or bypassed.
4. Run complete post-change focused tests and `docker compose config --quiet`.
5. Create the final evidence receipt, DCA learning proposal/entry, and durable Obsidian architecture note.

## Exact current source hashes

| Path | SHA-256 |
|---|---|
| `Dockerfile` | `5d22b5798202111d310b5edabf403969c8c371fa356703db25b6eb1d9bb22bba` |
| `docker-compose.yml` | `e8a452d44146d2bda094438165e03886dc1aa40609a5ca02842913bfb53b8c0d` |
| `scripts/odysseus-mcp` | `71f213b1acf11f6e8c9d6780c3897faf8ebf7d7f0b1d06417dd36373ad584f33` |
| `docker/lsp-runtime/package.json` | `a25c6e7061ac94444fe27f410845619c4898619e9b449602a8311776fc72d1d3` |
| `docker/lsp-runtime/package-lock.json` | `4b0a825759e39cb19cdaa421b8cd2e6dd8bc5f28d0dcab7ac8af2d7fc84fbe53` |
| `docker/lsp-runtime/npx` | `12bb24b17b000a13cbfc779e1528009c8ac859ccc3e8a5f0e4a849fd89012157` |
| `docker/lsp-runtime/verify_lsp_runtime.py` | `4a9269a4629ceb284dec606ebf074bf5c3581bf7f7a77e6355f055f15b374b02` |
| `tests/cli/test_mcp_cli_update.py` | `ef0dfbd6104545e1f3d6df1830a49f694b2a2f3414c0afa863b20e566236ad81` |
| `tests/fixtures/lsp_docker/python_fixture.py` | `1c7e843c74ca8640effac58523a5161693b76959ce8f8ba1edb396a93a3e37cc` |
| `tests/fixtures/lsp_docker/ts_fixture.ts` | `30114e022627e8b16e09148fe6382b5721500bb07cb68e4946eb0ad56bf074b9` |
| `data/app.db` at handoff observation | `70cab8c8dbec0ba2c888f23424cd783a6d2ea085d3c25961236c3da1e966bcf6` |

## Resume authority

```text
GO — resume only the LSP Docker closeout described in LSP_DOCKER_HANDOFF_20260807_01.md. Rebuild only odysseus to bind the current probe source, run its offline semantic definition/references/rename-dry-run checks with no network, re-hash fixtures, then use an already-authenticated Odysseus session if available to exercise the same tools through the live manager. Do not bypass auth, alter providers/ports/credentials, touch Dashboard/Bridge, or recreate any Compose service other than odysseus. Complete only the final receipts, DCA learning path, and Obsidian architecture note after real verification.
```
76 changes: 76 additions & 0 deletions LSP_DOCKER_HANDOFF_20260807_02.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# LSP Docker Activation — Current Handoff

**Created:** 2026-08-07<br>
**Chat scope:** LSP Docker activation for Odysseus only.<br>
**Status:** `LIVE_CONNECTION_VERIFIED / SEMANTIC_CLOSEOUT_PENDING`.

## What is live now

- Odysseus runs as Docker only on `127.0.0.1:7000`.
- Current container: `86b02519adf8a487ffe6be7f1d98946ec9d8df7d2979cd199ff686e05b11fb47`.
- Current image: `sha256:c2fb085dad45b173c093f8067485b1ff95639ba1fc6ac4c44380c4ea718f0d65`.
- `GET /api/health` returned `{"status":"healthy",...}`.
- Dashboard remains on `127.0.0.1:7001`; its prior response was the expected `302` authentication redirect.
- The existing LSP MCP record `396e33c2-e4a5-4fae-b60d-b1c1ca5ed0f7` is enabled and now uses:
- command: `node`
- argument: `/opt/odysseus-lsp/node_modules/@treedy/lsp-mcp/dist/index.js`
- Linux-only pinned/offline environment values, redacted by the supported CLI.
- Live application log evidence confirms: `LSP Code Intelligence (Pinned) ... 101 tools via stdio`.
- The live LSP Node process is present under the Odysseus container.

## Completed implementation

1. Added a Docker-internal, lockfile-installed LSP runtime under `/opt/odysseus-lsp`.
2. Pinned exact primary packages:
- `@treedy/lsp-mcp@0.2.8`
- `pyright@1.1.411`
- `@treedy/pyright-mcp@1.1.8`
- `@treedy/typescript-lsp-mcp@0.1.7`
3. Added an offline-only `npx` dispatcher that maps the upstream `@latest` backend requests to the approved pinned local packages and rejects undeclared requests.
4. Added a read-only Python/TypeScript fixture mount at `/workspace/lsp_fixture`.
5. Added and tested `odysseus-mcp update` to safely update explicit `args`, `env`, and `is_enabled` fields of the existing record; no raw SQLite edit was used.
6. Updated the stale Chrome build pin to the currently available exact package version `151.0.7922.108-1`; the Docker hardening test was updated with it.

## Verified evidence

- Focused source suite: `23 passed, 1 warning` before the final probe-only edits.
- Docker Compose configuration validated.
- Rebuilt Odysseus image successfully.
- Isolated container acceptance with `--network none` passed for:
- MCP initialization: `101` tools.
- Python Pyright backend startup: `1.1.8`.
- TypeScript backend startup: `0.1.7`.
- Python intentional undefined-symbol diagnostic.
- TypeScript intentional string-to-number diagnostic.
- Unknown package dispatcher rejection.
- Fixture source hashes remained unchanged after those offline diagnostics.
- Live in-container checks confirmed all four exact package versions and read-only fixture access.

## Rollback

| Path | SHA-256 | State |
|---|---|---|
| `G:\AIW\05_BAK\ODY_LSP_DOCKER_PRE_ACTIVATION_20260807_165042_02\app.db` | `303e42a612dbf04925289974e9c1ae46fcda6063d382f0a73105e56779200567` | SQLite `integrity_check=ok`; original LSP record disabled with its Windows host path |

The earlier `_01` backup directory is empty: its destination-path attempt failed before a database file was created. It was preserved; nothing was deleted.

## Exact outstanding work

Do **not** claim full semantic completion yet.

1. The current source probe was extended to test `definition`, `references`, and `rename` after the live image build.
2. The first new definition check used a coordinate that returned `No definition found at this position`; this is a test-fixture coordinate issue, not a runtime failure.
3. The next bounded action is to correct the probe to find the symbol coordinate deterministically, then run:
- Python definition;
- Python references;
- Python rename as workspace-edit/dry-run only;
- post-test fixture hashes.
4. Rebuild/recreate only `odysseus` once the finalized probe passes so its image exactly matches source.
5. An authenticated live manager semantic call remains unverified because `/api/mcp/servers` correctly returns `401`; no credentials or authentication settings were read, changed, or bypassed.
6. Then run final focused tests, create a final LSP receipt, route the learning entry through the approved DCA path, and add the stable Obsidian architecture note.

## Resume authority

```text
GO — resume only LSP Docker semantic closeout. Preserve the live loopback Odysseus service, dashboard, provider settings, credentials, ports, and unrelated containers. Fix the bounded probe coordinate, prove definition/references/rename-dry-run with network disabled, re-hash the read-only fixture, rebuild/recreate only odysseus to bind the final probe source, and finish receipts/DCA learning/vault closeout. Do not bypass Odysseus authentication or touch Dashboard/Bridge.
```
Loading
Loading