Skip to content

Finish runtime-aware container MCP launcher - #227

Merged
joshyorko merged 4 commits into
masterfrom
feat/container-mcp-launcher
Aug 27, 2026
Merged

Finish runtime-aware container MCP launcher#227
joshyorko merged 4 commits into
masterfrom
feat/container-mcp-launcher

Conversation

@joshyorko

@joshyorko joshyorko commented Aug 27, 2026

Copy link
Copy Markdown
Owner

Container MCP launcher

This PR finishes the committed container MCP launcher design from 5a7fd87.

Feature behavior:

  • The existing global --runtime selector now controls mcp codex preview, apply, status, and remove.
  • Native remains the default and preserves the existing direct-binary stdio snippet.
  • Container mode renders a deterministic Docker or Podman launcher with run, --rm, -i, --pull=missing, host UID:GID, a database-parent mount at /data, the selected image, /data/, and mcp stdio --read-only.
  • The existing read-only tool allowlist, approval mode, and 30-second startup/tool timeouts remain unchanged.
  • Generated snippets use explicit TOML escaping and are parsed as TOML.
  • Invalid runtime, database, mount, UID/GID, and representation inputs fail before config mutation with remediation guidance.
  • Preview and status do not write. Apply updates only the owned block, backs up drift, is idempotent, and does not pull or start a container. Remove deletes only the owned block and works without the old container runtime.

Documentation:

  • Restores the committed design document.
  • Adds docs/getting-started.md and links it near the top of README.
  • Documents the future-release Homebrew path, native and opt-in container paths, restart plus memory_status verification, switching, removal, uninstall, source-build fallback, and data retention.
  • Clearly states that Homebrew, release, image, tag, and tap artifacts are not published by this feature slice.

Scope and deferred work

The final diff is intentionally limited to README.md, docs/getting-started.md, the recovered design document, src/cli.rs, the minimal src/native_runtime.rs visibility change, and focused tests/cli_smoke.rs.

Deferred and excluded:

  • .github/workflows/release.yml
  • Dockerfile release/cache changes
  • Homebrew formula/tap changes
  • release artifacts, tags, GitHub releases, and image publication
  • unrelated public-handle/security, provider, Dreamer, temporal, and broad test changes

The original mixed branch tree and all supplied dirty-state recovery artifacts are preserved locally at:

/home/kdlocpanda/.codex/recovery/codex-memoryd/container-mcp-launcher-20260827/

The prior mixed branch is preserved as recovery/container-mcp-launcher-preintegration-20260827.

Verification

Passed:

  • Docker: cargo fmt --all -- --check
  • Docker: cargo test --locked --test cli_smoke mcp_codex — 19 passed
  • Docker: cargo test --locked --test cli_smoke getting_started — 1 passed
  • Docker: cargo test --locked toml_basic_string — 2 passed
  • Docker: cargo clippy --locked --test cli_smoke — completed with warnings only; warnings are pre-existing
  • Fresh detached checkout of exact commit b8bb09a: the focused MCP, documentation, and TOML tests passed
  • git diff --check
  • Design recovery byte comparison against 5a7fd87
  • git ls-remote verification of the pushed head

Full repository gates:

  • Docker cargo test --locked — failed after 79 library tests and 4 binary tests passed. Unchanged baseline failure: tests/adapter_conformance.rs::recall_is_advisory_budgeted_and_cited expects turn:recall-1 but receives generated msrc_.... The same test fails on untouched origin/master 5c6f60c.
  • Docker cargo clippy --all-targets --locked — failed on unchanged tests/dreamer.rs:1174 due clippy::unused_io_amount; existing warnings were also emitted.
  • No GitHub checks are currently reported for this branch.

Unavailable or intentionally unproved:

  • Host Cargo is unavailable; all Rust execution used the project Docker toolchain.
  • Actual Docker/Podman MCP image startup, image pull, SQLite sidecar ownership, and end-to-end Codex consumption were not run. The tests use deterministic fake runtimes because this slice must not publish an image or release artifact.
  • Podman host execution is permission-blocked by the host libpod runtime lock; this does not affect fake-runtime rendering tests.

Publication boundary

Published by this PR: the feature branch update and this pull request only.

Not performed: merge, tag, GitHub release, container image publication, Homebrew formula/tap modification, or Homebrew publication.

Base: master at 5c6f60c
Head: feat/container-mcp-launcher at b8bb09a

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5a7fd8786d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +82 to +84
codex-memoryd mcp codex preview --runtime container
codex-memoryd mcp codex apply --runtime container
codex-memoryd mcp codex status --runtime container

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Initialize the bind source before onboarding Codex

For a first-time container user, the default database parent (~/.codex-memoryd) does not yet exist, and this sequence never runs codex-memoryd init; the design also states that apply mutates only Codex config and does not start the container. Consequently, Codex reaches the bind mount before the application can create the directory, causing the mount to fail or be created with ownership that prevents the requested UID/GID from initializing SQLite. Add an initialization/directory-creation step to this path or explicitly make apply create the validated database parent.

Useful? React with 👍 / 👎.

Comment on lines +47 to +49
- image acquisition: pull when missing (`--pull=missing`)
- image: resolved `CODEX_MEMORYD_IMAGE`, defaulting to
`ghcr.io/joshyorko/codex-memoryd:latest`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Refresh mutable latest images

When the default :latest image has already been cached, --pull=missing deliberately reuses that cached image, so upgrading the Homebrew CLI or publishing a newer release will not update the MCP server and users can remain on an old, potentially schema-incompatible binary indefinitely. Either generate an immutable version tag tied to the installed CLI or use an always-pull policy for the mutable latest default.

Useful? React with 👍 / 👎.

Comment on lines +50 to +53
- identity: resolved host UID/GID so SQLite files remain host-owned
- storage: mount only the database's parent directory read-write at `/data`
- database argument: the corresponding `/data/<filename>` path
- MCP tier: explicit `mcp stdio --read-only`

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass the resolved configuration into the container

Mounting only the database directory and passing only --db makes the container load configuration from its own home rather than the host's selected/default config. Unlike the native entry, it therefore loses configured default profile/workspace and other runtime settings, so an existing database can appear empty or behave differently whenever the user relies on non-default configuration. Pass the necessary resolved settings as arguments/environment, or mount the selected config read-only and reference its in-container path.

Useful? React with 👍 / 👎.

Comment on lines +23 to +24
- `--runtime native` preserves the current direct-binary stdio entry.
- `--runtime container` emits a Docker or Podman stdio entry that starts the

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Generate an upgrade-stable Homebrew binary path

Preserving the current direct-binary entry can persist a versioned Homebrew Cellar executable path; after brew upgrade removes that keg, Codex again points at a binary that no longer exists—the exact failure this design is intended to repair. The native Homebrew path should render a stable opt symlink or another durable launcher path, and the compatibility test should distinguish source-build behavior from Homebrew upgrades.

Useful? React with 👍 / 👎.

Comment on lines +23 to +26
- `--runtime native` preserves the current direct-binary stdio entry.
- `--runtime container` emits a Docker or Podman stdio entry that starts the
published image on demand and exits when Codex closes stdin.
- Omitting `--runtime` preserves the resolved/default native behavior.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Define behavior for every accepted runtime value

The repository's global RuntimeKind also accepts auto and compose-dev, and because the option is global those values already parse for mcp codex; they can also be selected through runtime environment resolution. This decision specifies only native and container behavior, leaving valid invocations to be silently treated as native/container or rejected inconsistently. Specify how auto resolves and whether compose-dev is mapped or rejected before config mutation, and cover both values in the CLI tests.

Useful? React with 👍 / 👎.

Comment on lines +98 to +102
Container rendering fails before config mutation when:

- neither Docker nor Podman can be resolved;
- the database has no usable file name or parent directory;
- UID/GID or mount values cannot be represented safely.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Allow removal when the container runtime is unavailable

If Docker or Podman has been uninstalled or is temporarily unavailable while the resolved runtime remains container, requiring container rendering to succeed before any config mutation prevents mcp codex remove from deleting the stale MCP block even though removal needs neither an engine nor an image. Exempt remove from container prerequisite resolution so cleanup remains possible in the failure scenario it is meant to recover from.

Useful? React with 👍 / 👎.

Comment on lines +55 to +56
The existing Codex allowlist, approval mode, startup timeout, and tool timeout
remain unchanged. The database directory mount, rather than a single-file

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Increase startup time for first-use image pulls

The container path pulls a missing image synchronously during MCP startup but explicitly preserves the existing 30-second startup timeout. On a first launch, downloading and unpacking the image can readily exceed that limit, causing Codex to abort the MCP server before onboarding completes even though the runtime is functioning normally. Give container entries a longer startup timeout or pull the image during an explicit setup step before Codex starts it.

Useful? React with 👍 / 👎.

@joshyorko
joshyorko force-pushed the feat/container-mcp-launcher branch from 72f9bb5 to b8bb09a Compare August 27, 2026 14:05
@joshyorko joshyorko changed the title Design container MCP launcher onboarding Finish runtime-aware container MCP launcher Aug 27, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8bb09aade

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/cli.rs
Comment on lines +2613 to +2615
runtime.image.clone(),
"--db".to_string(),
format!("/data/{database_filename}"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass resolved settings to the container

When CODEX_MEMORYD_PROFILE, CODEX_MEMORYD_WORKSPACE, or other settings come from the host config/runtime.env, this argv passes only the database path, so the image loads its own defaults and recalls from personal/default rather than the selected scope. The newly implemented argument vector is fresh evidence beyond the earlier design comment: either pass the resolved settings as container environment variables or mount and select the resolved configuration.

Useful? React with 👍 / 👎.

Comment thread src/cli.rs
"container MCP database parent cannot be represented safely in a Docker/Podman --volume mount: {parent}; use a path without ':' or control characters, or set CODEX_MEMORYD_DB to a safe directory"
)));
}
Ok((parent.to_string(), filename.to_string()))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve symlinked database files before mounting

When the configured database file is a symlink whose target is outside its parent directory, only the parent is canonicalized and mounted while the original symlink filename is retained. Inside the container /data/<filename> then resolves to an unmounted host path and MCP startup cannot open the database; canonicalize an existing database file and mount its target parent/name, or reject symlinks that escape the mount.

Useful? React with 👍 / 👎.

Comment thread src/cli.rs
"Codex MCP does not support --runtime compose-dev; choose --runtime native or --runtime container",
)),
RuntimeKind::Container => {
let command = native_runtime::container_runtime(runtime).map_err(|err| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Persist an absolute container runtime path

When apply discovers Docker or Podman through the invoking shell's PATH, container_runtime returns the literal name (docker or podman), which is then persisted as the Codex command. If restarted Codex has a different PATH—for example, when launched as a desktop application—the generated MCP entry cannot find an engine that successfully passed validation during apply; configured relative wrapper paths have the same problem after the working directory changes. Resolve the executable to an absolute path before rendering the entry.

Useful? React with 👍 / 👎.

Comment thread src/cli.rs Outdated
Comment on lines +2609 to +2610
"--user".to_string(),
format!("{uid}:{gid}"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve host identity under rootless Podman

When the selected engine is rootless Podman, passing the host numeric UID/GID via --user does not preserve host identity: those container IDs are mapped through the rootless user namespace to subordinate host IDs, so the process commonly cannot create SQLite WAL files in the host-owned bind directory. Podman's keep-id user namespace mode maps the current rootless user's UID/GID to the same values inside the container; generate engine-specific identity arguments instead of applying Docker's --user strategy to both engines.

Useful? React with 👍 / 👎.

Comment thread src/cli.rs Outdated
Comment on lines +2604 to +2608
let args = vec![
"run".to_string(),
"--rm".to_string(),
"-i".to_string(),
"--pull=missing".to_string(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Disable the HTTP healthcheck for stdio containers

For every MCP session lasting beyond the image's healthcheck interval, the inherited Dockerfile:55-57 check repeatedly requests 127.0.0.1:8787/healthz, but this invocation runs mcp stdio and never starts an HTTP listener. The otherwise functional MCP container is therefore inevitably marked unhealthy, which misleads operators and can trigger health-based automation; disable the image healthcheck for this launcher or provide a healthcheck appropriate to stdio mode.

Useful? React with 👍 / 👎.

Comment thread src/cli.rs
database.display()
))
})?;
let metadata = std::fs::metadata(parent).map_err(|err| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject an existing directory as the database

When CODEX_MEMORYD_DB points to an existing directory, this validation checks only that path's parent and then treats the directory's basename as the database filename. apply therefore succeeds and mutates Codex configuration, but the later MCP process attempts to open a directory as SQLite at /data/<basename> and fails during startup; inspect the database path itself and reject it when it is a directory.

Useful? React with 👍 / 👎.

@joshyorko
joshyorko merged commit 212de3a into master Aug 27, 2026
@joshyorko
joshyorko deleted the feat/container-mcp-launcher branch August 27, 2026 23:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant