Skip to content

fix: carry the IDE entry's env when wiring the datamate stdio MCP server - #1081

Open
ralphstodomingo wants to merge 12 commits into
mainfrom
fix/datamate-stdio-env
Open

fix: carry the IDE entry's env when wiring the datamate stdio MCP server#1081
ralphstodomingo wants to merge 12 commits into
mainfrom
fix/datamate-stdio-env

Conversation

@ralphstodomingo

@ralphstodomingo ralphstodomingo commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #1082

Type of change

  • Bug fix

What does this PR do?

Fixes the bug where datamate-cli.js suddenly opens as an editor tab when launching sessions, and the datamate MCP server dies with -32000 Connection closed.

On desktop editors the extension-written .vscode/mcp.json datamate stdio entry has command = the editor's Electron binary and env: {"ELECTRON_RUN_AS_NODE": "1"} (Electron only runs the script as Node with that flag; without it, the editor GUI boots and opens the script as a document). datamate_manager add reused the entry's command + args but dropped the env block, both in the immediate spawn and in the entry persisted to .altimate-code/altimate-code.json — so the file popped on add and again on every later session launch, with no self-repair in TUI/run (the healing sync only ran on serve boot).

Changes:

  • readDatamateTransportFromIde now returns the IDE entry's env (minus ALTIMATE_EXTENSION_RPC, mirroring the sync path) and updatedAt; handleAdd carries the env into the runtime MCP config and persists it as environment, plus updatedAt on disk so the sync recognizes the entry as current.
  • The sync path's inline env-strip is extracted into a shared extractSpawnEnvironment helper so the two paths stay in lockstep.
  • The TUI worker and run now run syncDatamateUrlFromVscodeMcp before the first session, as serve already did — entries already persisted broken in the field self-heal on the next launch. The heal is scoped to the containing git project root (resolveDatamateSyncRoot, bounded at the home directory), walks the config files from the launch directory up to that root the way the loader does (a nested package's own config is healed too), and in the worker it is sequenced strictly before config load, the first in-process request, and Server.listen, so the first session connects with the healed entry rather than a stale cached one. datamate_manager add on an existing-but-disconnected entry likewise refreshes it from the current IDE transport before connecting.
  • Trust boundary (from review): the env carry is an allowlistELECTRON_RUN_AS_NODE only, since the carried env is spread over the host process env at spawn. Transport sources are only the two locations the extension writes (**/.vscode/mcp.json, **/.cursor/mcp.json), parsed through a validating parseIdeTransport (local needs a non-empty command, remote a non-empty url; blank tombstones and incomplete entries are skipped rather than winning selection — an incomplete entry used to be persisted as a url-less remote). Entries derived from an IDE file carry provenance (managedBy: "altimate-ide" + sourceMcpJson), and the boot heal never rewrites a global entry from a project file unless that entry's stamp matches the exact IDE file — hand-added and legacy global entries are left alone; an explicit datamate_manager add is what (re)stamps them and is the remedy for a legacy global entry.
  • Scope note: everything above is datamate-specific except one known side effect of the wider sync trigger — syncDatamateUrlFromVscodeMcp has a second pass that refreshes the URL (and updatedAt) of other remote MCP entries mirrored from the IDE config (name match, URL differs). That pass is not new behavior — serve boot has always run it — TUI/run now just apply the same refresh consistently. Spawn/env behavior for non-datamate servers is unchanged.

How did you verify your code works?

E2E in the docker code-server harness against a desktop-shaped mcp.json entry (command = an Electron-contract shim that opens its args as documents unless ELECTRON_RUN_AS_NODE=1), driven through real run sessions:

Scenario Published 0.8.10 This branch
datamate_manager add (project) file pops, -32000 Connection closed, env-less entry persisted no pop, connected as 'datamate', entry carries environment + provenance
Plain launch on the persisted entry file pops on every launch
Broken project entry, plain launch healed before connect, no pop
Broken global entry stamped from this project's mcp.json, plain launch healed, no pop
Legacy/hand-added global entry (no provenance), plain launch left untouched by design (pops until the explicit add below)
Explicit datamate_manager add --scope global on that legacy entry, then relaunch entry restamped + healed, relaunch is clean
Blanked .cursor/mcp.json tombstone sorting first healed, no pop
Published 0.8.10 launched on a healed entry no pop (heal is one-way — a reporter who tested a fix build can no longer reproduce)

Unit tests: test/release-validation/mcp-datamate-stdio-env.test.ts covers the env carry (strip rule, omission when empty, back-compat bare shape, non-string filtering) and sync parity. Existing mcp-datamate-893 suite unchanged and green; tsgo --noEmit clean.

Screenshots / recordings

Before — datamate_manager add pops the file open:

before

After — same broken persisted entry, next session heals it and nothing pops:

after

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

`datamate_manager add` reused the command + args from the IDE's `mcp.json`
`datamate` entry but dropped its `env` block, both in the immediate spawn and
in the entry persisted to `.altimate-code/altimate-code.json`. On desktop
editors the command is the editor's Electron binary and `env` carries
`ELECTRON_RUN_AS_NODE=1` — spawned without it, the editor GUI boots and opens
`datamate-cli.js` as a document, the MCP client reports `-32000 Connection
closed`, and the broken persisted entry re-pops the file on every subsequent
session launch.

- `readDatamateTransportFromIde` now returns the entry's env (minus
  `ALTIMATE_EXTENSION_RPC`, mirroring the sync path) and `updatedAt`;
  `handleAdd` carries the env into the runtime config and persists it as
  `environment`, plus `updatedAt` on disk so the sync recognizes the entry
  as current.
- The sync path's inline env-strip is extracted into the shared
  `extractSpawnEnvironment` helper so both paths stay in lockstep.
- The TUI worker and `run` now run `syncDatamateUrlFromVscodeMcp` before the
  first session (as `serve` already did), so entries already persisted
  without `environment` self-heal on the next launch.
@ralphstodomingo ralphstodomingo self-assigned this Aug 7, 2026
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Datamate local transports now preserve filtered environment variables and updatedAt. Run and TUI startup paths synchronize VS Code MCP configuration before use. Regression tests cover environment filtering, Git-root resolution, and persisted transport metadata.

Changes

Datamate synchronization

Layer / File(s) Summary
Transport metadata and discovery
packages/opencode/src/altimate/datamate-transport.ts
Local and remote transports support updatedAt. Local transports support filtered environment values. Discovery and synchronization preserve valid values and resolve the Git project root when available.
Configuration synchronization and persistence
packages/opencode/src/altimate/datamate-transport.ts, packages/opencode/src/altimate/tools/datamate.ts
Synchronization preserves filtered environment values and timestamps. Existing entries retain non-transport fields, set enabled: true, write refreshed configuration, and reconnect with MCP.add().
Startup synchronization and validation
packages/opencode/src/cli/cmd/run.ts, packages/opencode/src/cli/tui/worker.ts, packages/opencode/test/release-validation/mcp-datamate-stdio-env.test.ts, packages/opencode/test/release-validation/mcp-datamate-893.test.ts
Run and TUI paths perform best-effort synchronization before startup, RPC fetches, and external server startup. Tests cover environment conversion, root resolution, global configuration, and persisted metadata.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested reviewers: anandgupta42

Sequence Diagram(s)

sequenceDiagram
  participant RunCommand
  participant DatamateTransport
  participant MCPConfig
  participant DatamateGateway
  RunCommand->>DatamateTransport: resolve project root
  RunCommand->>MCPConfig: synchronize Datamate entry
  MCPConfig->>DatamateTransport: read command, environment, and updatedAt
  DatamateTransport-->>MCPConfig: return filtered transport metadata
  MCPConfig->>DatamateGateway: persist refreshed entry
  DatamateGateway-->>RunCommand: complete or suppress synchronization error
  RunCommand->>DatamateGateway: start local session
Loading

Poem

A rabbit keeps the Node flag bright,
Filters stray variables from sight.
Timestamps hop into the stream,
Startup entries heal the scheme.
MCP runs without surprise.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #1082 by preserving the IDE environment, repairing persisted entries, and synchronizing before session startup.
Out of Scope Changes check ✅ Passed The changes remain within Datamate transport synchronization and environment handling, including the stated global configuration support.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly summarizes the primary change: preserving the IDE entry environment when wiring the Datamate stdio MCP server.
Description check ✅ Passed The description follows the repository template. It includes the issue, change type, detailed implementation rationale, verification results, screenshots, and completed checklist.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/datamate-stdio-env

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

Thanks for updating your PR! It now meets our contributing guidelines. 👍

@AltimateAI AltimateAI deleted a comment from github-actions Bot Aug 7, 2026
@ralphstodomingo
ralphstodomingo marked this pull request as ready for review August 7, 2026 04:26
Copilot AI review requested due to automatic review settings August 7, 2026 04:26

@claude claude 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.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

Copilot AI 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.

Pull request overview

This PR fixes a desktop-editor regression where the IDE-provided datamate stdio MCP entry’s env (notably ELECTRON_RUN_AS_NODE=1) was dropped when wiring/persisting the server, causing Electron to boot the editor UI and open datamate-cli.js as a tab, and leading to -32000 Connection closed. It also expands the “heal from .vscode/mcp.json” sync behavior so terminal entrypoints (run/TUI worker) self-repair already-persisted broken entries, matching serve startup behavior.

Changes:

  • Carry the IDE env (minus ALTIMATE_EXTENSION_RPC) and updatedAt through readDatamateTransportFromIde, datamate_manager add runtime wiring, and persisted config.
  • Deduplicate env-stripping logic into a shared extractSpawnEnvironment() helper to keep add and sync paths aligned.
  • Trigger syncDatamateUrlFromVscodeMcp earlier for run and the TUI worker so previously-broken persisted entries self-heal on next launch.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/opencode/src/altimate/datamate-transport.ts Adds env + updatedAt propagation for IDE datamate stdio entries; factors env normalization into extractSpawnEnvironment; updates sync to use shared env extraction.
packages/opencode/src/altimate/tools/datamate.ts Ensures datamate_manager add carries environment into runtime MCP config and persists environment + updatedAt to disk.
packages/opencode/src/cli/tui/worker.ts Adds a boot-time datamate sync gate so the worker doesn’t serve requests / start external server mode until the heal attempt finishes.
packages/opencode/src/cli/cmd/run.ts Runs the same datamate sync before bootstrapping a session to self-heal env-less persisted entries.
packages/opencode/test/release-validation/mcp-datamate-stdio-env.test.ts Adds regression coverage for env carry-through, stripping rules, back-compat, and sync parity.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/opencode/src/cli/tui/worker.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
packages/opencode/src/altimate/datamate-transport.ts 41 Transport scan narrowed to .vscode/.cursor, dropping .github/copilot/mcp.json which the tool's docs and mcp/discover.ts still list as a supported IDE location
Files Reviewed (4 files)
  • packages/opencode/src/altimate/datamate-transport.ts - 1 issue
  • packages/opencode/src/altimate/tools/datamate.ts
  • packages/opencode/src/mcp/config.ts
  • packages/opencode/test/release-validation/mcp-datamate-893-codex.test.ts
  • packages/opencode/test/release-validation/mcp-datamate-893.test.ts
  • packages/opencode/test/release-validation/mcp-datamate-stdio-env.test.ts

Fix these issues in Kilo Cloud

Previous Review Summaries (11 snapshots, latest commit 692417a)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 692417a)

Status: No Issues Found | Recommendation: Merge

Incremental review of 9b167348..692417ac — commit 692417ac5 ("refactor: share the blank-tombstone predicate between both mcp.json scans").

The commit resolves the prior review's only SUGGESTION: the duplicated blank-tombstone predicate is now a single isBlankDatamateEntry helper (datamate-transport.ts:53), used by both scan sites. readDatamateTransportFromIde (line 166) skips blank entries via if (isBlankDatamateEntry(entry)) continue, and syncDatamateUrlFromVscodeMcp (line 245) selects a non-blank source via if (!isBlankDatamateEntry(map[DATAMATE_KEY])) — the same logic as before, in lockstep. The helper additionally guards typeof entry !== "object", a strictly more defensive (and correct) check since a non-object value can never be a valid MCP server entry. Behavior is otherwise identical; no drift risk remains.

Files Reviewed (1 file)
  • packages/opencode/src/altimate/datamate-transport.ts

Previous review (commit 9b16734)

Status: 1 Issue Found | Recommendation: Address before merge

Incremental review of 7f684289..9b167348 — commit 9b1673481 ("fix: skip blanked {} datamate entries when selecting the mcp.json source").

The extension blanks datamate to {} (a tombstone) in non-active-IDE mcp.json files, and the sorted scan can reach the blanked file first (.cursor/ sorts before .vscode/). Both scan sites — readDatamateTransportFromIde (line 159) and syncDatamateUrlFromVscodeMcp (line 239) — now skip empty entries so the active IDE's real entry is found. Previously a blanked entry short-circuited the read scan (returning a fallback marker) and made the sync silently no-op while selecting the wrong file. The fix is correct and well-covered by two new tests. One minor maintainability nit on the duplicated predicate.

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/opencode/src/altimate/datamate-transport.ts 239 Blank-tombstone predicate duplicated at both scan sites
Files Reviewed (2 files)
  • packages/opencode/src/altimate/datamate-transport.ts - 1 issue
  • packages/opencode/test/release-validation/mcp-datamate-stdio-env.test.ts

Fix these issues in Kilo Cloud

Previous review (commit 7f68428)

Status: No Issues Found | Recommendation: Merge

Incremental review of 42311f23..7f684289 — commit 7f6842891 ("test: update reload-endpoint source guard for the multi-path disk read").

Test-only change: the adversarial UPI-25..27 suite's source-text guard for the /altimate/mcp/reload-datamate endpoint is updated to match the multi-path disk read that landed in earlier commits. The old single-path assertion (const freshEntry = await readMcpEntryFromDisk(name, configPath)) was already stale — the shipped endpoint scans every config file via findAllConfigPaths(directory, Global.Path.config) and loops until it finds the entry (server.ts:694–712). The new assertions (const configPaths = await findAllConfigPaths(...), freshEntry = await readMcpEntryFromDisk(name, configPath), await MCP.add(name, freshEntry)) were each verified verbatim against the current source. The stale-singleton bypass contract asserted by the surrounding checks is unchanged. No issues on changed lines.

Files Reviewed (1 file)
  • packages/opencode/test/upstream/adversarial/upi-config-mcp.test.ts

Previous review (commit 42311f2)

Status: No Issues Found | Recommendation: Merge

Incremental review of 6625177f..42311f23 — commit 42311f23 ("scope legacy config.json to global config candidates only").

The split is correct: config/config.ts loadGlobal merges config.json from the global dir (config.ts:360), but the project loader (ConfigPaths.files searches only opencode.json{,c} at paths.ts:24, and the .altimate-code/.opencode loop reads only altimate-code.json{,c} + opencode.json{,c} at config.ts:538-545) never reads a project-level config.json. Gating config.json on the global scope in both resolveConfigPath and findAllConfigPaths stops the heal from discovering/writing an entry the loader would ignore, and the default write target is unaffected (config.json was the tail candidate). Covered by a byte-identity regression test. No issues on changed lines.

Files Reviewed (2 files)
  • packages/opencode/src/mcp/config.ts
  • packages/opencode/test/release-validation/mcp-datamate-stdio-env.test.ts

Previous review (commit 6625177)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (6 files)
  • packages/opencode/src/altimate/datamate-transport.ts
  • packages/opencode/src/cli/cmd/run.ts
  • packages/opencode/src/cli/tui/worker.ts
  • packages/opencode/src/mcp/config.ts
  • packages/opencode/src/server/server.ts
  • packages/opencode/test/release-validation/mcp-datamate-stdio-env.test.ts

Previous review (commit 33b60d8)

Status: 1 Issue Found | Recommendation: Merge (1 non-blocking suggestion)

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/opencode/src/altimate/datamate-transport.ts 331 A throw on one config file aborts healing of the rest
Files Reviewed (3 files)
  • packages/opencode/src/altimate/datamate-transport.ts - 1 suggestion
  • packages/opencode/test/release-validation/mcp-datamate-893.test.ts
  • packages/opencode/test/release-validation/mcp-datamate-stdio-env.test.ts

The incremental commit (37c3d2c..33b60d8) extends the datamate heal to run across every config file (project + global) instead of just the project one, via findAllConfigPaths(cwd, globalConfigDir) and an extracted healEntryInFile helper. The global-dir default (Global.Path.config) matches the convention used by the sibling callers; the new "already up to date" / "synced" logs now include configPath; and updated reports DATAMATE_KEY once even when multiple files are healed. Tests cover global-only and project+global-in-one-pass healing. The only finding is a non-blocking robustness suggestion on the new loop.

Fix these issues in Kilo Cloud

Previous review (commit 37c3d2c)

Status: No Issues Found | Recommendation: Merge

The incremental commit (37c3d2c) hoists the duplicated updatedAt conditional spread from both handleAdd branches into a single shared updatedAtField constant computed once at the top of the IDE/extension-mode block. This is a clean, behavior-preserving refactor that resolves the prior DRY suggestion. transport.updatedAt (non-null inside the transport !== null branch) replaces the now-redundant transport?.updatedAt optional chaining, and the explanatory comment was consolidated at the declaration site.

Files Reviewed (1 file)
  • packages/opencode/src/altimate/tools/datamate.ts

Previous review (commit 7bcc9b6)

Status: 1 Issue Found | Recommendation: Merge (non-blocking suggestion)

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/opencode/src/altimate/tools/datamate.ts 284 Duplicated updatedAt conditional spread in both handleAdd branches (also at L303)

The incremental commit (7bcc9b6) correctly extends the datamate env/transport fix to remote transports and fixes a real inconsistency where the live MCP.add client dropped preserved auth/connection settings that the disk write kept. Verified sound:

  • DatamateTransport's remote variant now carries updatedAt?, and readDatamateTransportFromIde returns it for remote entries — parity with the local branch.
  • Both handleAdd updatedAt conditions generalize from transport?.type === "local" && … to transport?.updatedAt, matching the type change.
  • The refresh path's MCP.add now receives the merged refreshed entry instead of the bare mcpConfig. create() only short-circuits on enabled === false, so enabled: true connects exactly as before, and updatedAt/enabled are harmless extra keys in the in-memory s.config (not schema-validated at add, and the disk write is already handled separately by addMcpToConfig).

Only a minor DRY suggestion remains.

Fix these issues in Kilo Cloud

Files Reviewed (3 files)
  • packages/opencode/src/altimate/datamate-transport.ts
  • packages/opencode/src/altimate/tools/datamate.ts
  • packages/opencode/test/release-validation/mcp-datamate-stdio-env.test.ts

Previous review (commit 1cb8fad)

Status: No Issues Found | Recommendation: Merge

The incremental commit (1cb8fad) is a focused refactor that extracts the previously-duplicated TRANSPORT_FIELDS set into a single shared, exported TRANSPORT_IDENTITY_FIELDS constant in datamate-transport.ts, consumed by both syncDatamateUrlFromVscodeMcp and datamate_manager add's refresh path. This directly resolves the prior review's only SUGGESTION (drift risk between the two local sets).

Behavior is verified identical at both call sites:

  • Sync path: old set {type, command, args, environment, url, updatedAt}TRANSPORT_IDENTITY_FIELDS (same 6 fields).
  • handleAdd refresh: old set {…6 fields…, enabled}new Set([...TRANSPORT_IDENTITY_FIELDS, "enabled"]) (same 7 fields).

No new issues introduced; the enabled-added-locally rationale is documented inline.

Files Reviewed (2 files)
  • packages/opencode/src/altimate/datamate-transport.ts
  • packages/opencode/src/altimate/tools/datamate.ts

Previous review (commit 80d4ad4)

Status: 1 Issue Found | Recommendation: Merge (non-blocking)

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1

The incremental changes (commit 80d4ad4c) correctly resolve the prior review's concerns: the heal is now scoped to the git project root (resolveDatamateSyncRoot) so subdirectory launches find the IDE config + persisted entry, the TUI worker sequences the heal strictly before InstanceRuntime.load/Config.get() (removing the concurrent read/write window), and the in-config-but-not-connected branch now refreshes the persisted entry from the current IDE transport via the established readMcpEntryFromDisk + MCP.add pattern (matching the reload-datamate endpoint) before reconnecting. The primary local-stdio ELECTRON_RUN_AS_NODE fix is sound. Only one minor maintainability nit below.

Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/opencode/src/altimate/tools/datamate.ts 273 TRANSPORT_FIELDS duplicates the set in datamate-transport.ts:258; drift risk
Files Reviewed (5 files)
  • packages/opencode/src/altimate/datamate-transport.ts
  • packages/opencode/src/altimate/tools/datamate.ts
  • packages/opencode/src/cli/cmd/run.ts
  • packages/opencode/src/cli/tui/worker.ts
  • packages/opencode/test/release-validation/mcp-datamate-stdio-env.test.ts

Fix these issues in Kilo Cloud

Previous review (commit cbf4f65)

Status: No Issues Found | Recommendation: Merge

The fix correctly carries the IDE mcp.json env block (notably ELECTRON_RUN_AS_NODE) through both the datamate_manager add path and the mcp.json sync path. The refactor extracts a shared extractSpawnEnvironment helper that is behaviorally equivalent to the prior inline strip for normal cases while additionally filtering non-string values and validating the object shape — a strict, non-regressing improvement. updatedAt is persisted disk-only in handleAdd, matching how syncDatamateUrlFromVscodeMcp already records it, and the new TUI/run heal is awaited before the first session/connect in the correct order using process.cwd() consistently. Fork-only files need no altimate_change markers, and the run.ts/worker.ts additions are correctly wrapped. The new test uses await using tmpdir() (proper disposal) and covers the strip rule, empty-env omission, back-compat bare shape, non-string filtering, and sync parity.

Files Reviewed (5 files)
  • packages/opencode/src/altimate/datamate-transport.ts
  • packages/opencode/src/altimate/tools/datamate.ts
  • packages/opencode/src/cli/cmd/run.ts
  • packages/opencode/src/cli/tui/worker.ts
  • packages/opencode/test/release-validation/mcp-datamate-stdio-env.test.ts

Reviewed by deepseek-v4-pro · Input: 82.9K · Output: 25.9K · Cached: 1.2M

Review guidance: REVIEW.md from base branch main

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 5 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/src/cli/tui/worker.ts
Comment thread packages/opencode/src/altimate/tools/datamate.ts
Comment thread packages/opencode/src/altimate/datamate-transport.ts Outdated
Comment thread packages/opencode/src/cli/cmd/run.ts
Comment thread packages/opencode/src/cli/cmd/run.ts
Comment thread packages/opencode/src/cli/tui/worker.ts
Comment thread packages/opencode/src/cli/tui/worker.ts
…root sync scope

- TUI worker: the datamate heal is now sequenced strictly before
  `InstanceRuntime.load`/`Config.get()` (trace init awaits it), so the config
  read can neither race the non-atomic write nor cache the pre-heal entry —
  the first session connects with the healed config.
- `datamate_manager add`: the in-config-but-not-connected branch refreshes the
  persisted entry from the current IDE transport (preserving user-managed
  fields) and connects via `MCP.add`, instead of `MCP.connect` which re-reads
  the stale in-memory entry.
- Boot heals (`run`, TUI worker) scan from the containing git project root via
  the new `resolveDatamateSyncRoot`, not raw cwd — a session launched from a
  subdirectory now finds the root IDE config and persisted entry.
Comment thread packages/opencode/src/altimate/tools/datamate.ts Outdated
…dd refresh

Both paths encode the same idea — entry fields re-derived from the IDE
transport versus user-managed fields carried forward. A single exported set
keeps them from silently diverging when a new transport field is added;
the add-refresh path layers `enabled` on top since it re-derives that too.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/opencode/src/altimate/tools/datamate.ts Outdated
Comment thread packages/opencode/src/altimate/tools/datamate.ts Outdated
Comment thread packages/opencode/src/altimate/tools/datamate.ts Outdated
…rry updatedAt for remote

- The add-refresh path wrote the merged entry (preserved headers/oauth/timeout
  + fresh transport) to disk but connected the live client with the bare
  transport config, dropping authentication and connection settings for the
  session being connected. MCP.add now receives the same merged entry as the
  disk write, matching the reload-datamate endpoint.
- The remote transport variant now carries updatedAt like the local one, so a
  remote datamate added via datamate_manager is not rewritten once by the next
  boot's sync purely for the missing change signal.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/opencode/src/altimate/datamate-transport.ts (1)

277-284: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Serialize MCP config writes before this sync path.

addMcpToConfig reads mcpConfig, then writes with modify + Filesystem.write without a lock. Concurrent datamate_manager add writes to the same server can overwrite newer fields such as environment, updatedAt, or user-managed headers/oauth/timeout. Add a per-config-path lock or update queue that covers IDE sync and datamate_manager add, and keep the lock shared when resolveConfigPath points to the same file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/opencode/src/altimate/datamate-transport.ts` around lines 277 - 284,
Serialize the read-modify-write flow in addMcpToConfig with a per-config-path
lock or update queue covering both IDE synchronization and datamate_manager add
operations. Ensure resolveConfigPath results sharing the same file reuse the
same lock, and hold it through mcpConfig reads, modify, and Filesystem.write so
newer environment, updatedAt, headers, oauth, and timeout fields are not
overwritten.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/opencode/src/altimate/datamate-transport.ts`:
- Around line 23-24: Update syncDatamateUrlFromVscodeMcp to compare the datamate
entry’s TRANSPORT_IDENTITY_FIELDS whenever readDatamateTransportFromIde returns
a transport without vscodeUpdatedAt, while preserving timestamp-based
synchronization when the timestamp is present. Add a regression test covering a
timestamp-less IDE transport and verifying that altimate-code.json is
synchronized.

---

Outside diff comments:
In `@packages/opencode/src/altimate/datamate-transport.ts`:
- Around line 277-284: Serialize the read-modify-write flow in addMcpToConfig
with a per-config-path lock or update queue covering both IDE synchronization
and datamate_manager add operations. Ensure resolveConfigPath results sharing
the same file reuse the same lock, and hold it through mcpConfig reads, modify,
and Filesystem.write so newer environment, updatedAt, headers, oauth, and
timeout fields are not overwritten.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5cbe1fe5-948b-4030-829b-bd6729445c96

📥 Commits

Reviewing files that changed from the base of the PR and between 80d4ad4 and 7bcc9b6.

📒 Files selected for processing (3)
  • packages/opencode/src/altimate/datamate-transport.ts
  • packages/opencode/src/altimate/tools/datamate.ts
  • packages/opencode/test/release-validation/mcp-datamate-stdio-env.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/opencode/test/release-validation/mcp-datamate-stdio-env.test.ts
  • packages/opencode/src/altimate/tools/datamate.ts

Comment thread packages/opencode/src/altimate/datamate-transport.ts Outdated
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

Re CodeRabbit's outside-diff finding (serialize addMcpToConfig writes): valid hardening suggestion, but the lock-free read-modify-write predates this PR — sync (serve boot + reload endpoint) and datamate_manager add have always been able to interleave across processes. Within a process this PR makes ordering stricter, not looser: the boot heal is sequenced before the first session, so it cannot run concurrently with a session-invoked add. A per-config-path write queue is parked as a follow-up rather than grown into this fix.

Comment thread packages/opencode/src/altimate/tools/datamate.ts Outdated
ralphstodomingo added 2 commits August 7, 2026 14:01
Both the refresh and new-entry branches persisted the transport's updatedAt
with the same conditional spread; a single `updatedAtField` above the branch
keeps them from drifting, and the disk-only rationale is documented once.
datamate_manager add supports scope "global", so a broken (env-less)
datamate entry can live in the global altimate-code.json. It is spawned at
session start like any merged config entry — reproducing the editor-tab pop —
but the boot heal only rewrote the project config, so the entry never
repaired (found by the bug reporter testing the fix: no environment block
appeared). syncDatamateUrlFromVscodeMcp now heals every config file carrying
a datamate entry via findAllConfigPaths (project, project subdirs, global),
reporting the entry once. Sync tests pass an isolated global dir so test runs
never touch the developer's real config.

@coderabbitai coderabbitai 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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/opencode/src/altimate/datamate-transport.ts`:
- Around line 331-332: Resolve the Git project root once at the start of
syncDatamateUrlFromVscodeMcp, then use that root instead of cwd for both
findAllMcpJsonFiles and findAllConfigPaths. Add a direct regression test
invoking syncDatamateUrlFromVscodeMcp from a nested directory and verify
root-level configuration files are synchronized.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: edd49936-8519-4d1c-ac2f-62ff387826f9

📥 Commits

Reviewing files that changed from the base of the PR and between 37c3d2c and 33b60d8.

📒 Files selected for processing (3)
  • packages/opencode/src/altimate/datamate-transport.ts
  • packages/opencode/test/release-validation/mcp-datamate-893.test.ts
  • packages/opencode/test/release-validation/mcp-datamate-stdio-env.test.ts

Comment thread packages/opencode/src/altimate/datamate-transport.ts Outdated
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

Field testing by the bug reporter surfaced a second gap, fixed in 33b60d8: datamate_manager add supports scope: "global", and a broken entry living in the global config (~/.config/altimate-code/altimate-code.json) is spawned at session start like any merged entry — reproducing the pop — but the boot heal only rewrote the project config, so the reporter saw no environment block appear. The sync now heals every config file carrying a datamate entry (findAllConfigPaths: project, project subdirs, global). Covered by new unit tests (global-only and project+global in one pass, with an isolated global dir so test runs never touch the developer's real config) and re-verified end-to-end in the code-server harness: a globally-scoped broken entry now gains environment at boot and nothing pops.

Comment thread packages/opencode/src/altimate/datamate-transport.ts Outdated

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 3 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/opencode/src/altimate/datamate-transport.ts Outdated
Comment thread packages/opencode/src/altimate/datamate-transport.ts Outdated
Comment thread packages/opencode/src/altimate/datamate-transport.ts
Comment thread packages/opencode/src/altimate/datamate-transport.ts Outdated
…al root resolution, per-file isolation, global-aware reload

- CONFIG_FILENAMES now mirrors every filename the config loader merges
  (adds altimate-code.jsonc and legacy config.json), so entries in those
  files are healed/removed/listed like the rest instead of loading as live
  config that tooling cannot see.
- syncDatamateUrlFromVscodeMcp resolves the git project root itself, so
  every caller (serve, reload endpoint, TUI worker, run) handles nested
  invocations; the worker/run callers drop their now-redundant resolution.
- One malformed config file no longer aborts the multi-file heal — each
  file is healed independently with a logged skip on failure.
- The reload-datamate endpoint reads the fresh entry from any config file
  the sync covers (project, subdirs, global) instead of only the project
  path, so a healed global-only entry actually reconnects.
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

Re cubic's four P2s on the global-heal commit — all four were valid, addressed in 6625177:

  1. Config filename coverage: CONFIG_FILENAMES now mirrors every filename the config loader merges (adds altimate-code.jsonc + legacy config.json), so entries in those files heal like the rest — and datamate_manager's remove/list paths see them too.
  2. Nested invocation: the sync resolves the git project root internally now, covering all callers (also CodeRabbit's inline finding).
  3. Per-file isolation: one malformed config no longer aborts the multi-file heal — each file heals independently with a logged skip (addMcpToConfig throwing on unparseable files is exactly the case).
  4. Global-aware reload: the reload-datamate endpoint reads the fresh entry from any config file the sync covers instead of only the project path, so a healed global-only entry actually reconnects.

Each has a regression test (nested-heal, malformed-continue, .jsonc global heal); suites green, no new failures vs main.

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 6 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.
Tip: cubic used a learning from your PR history. Let your coding agent read cubic learnings directly with the cubic MCP.

Re-trigger cubic

Comment thread packages/opencode/src/mcp/config.ts Outdated
The config loader merges config.json only from the global config dir; the
project loader reads only altimate-code.json/.jsonc and opencode.json/.jsonc.
Listing config.json in the shared filename set made project-side discovery
treat any unrelated project config.json as live config — and resolveConfigPath
could return it as the write target for a fresh add, persisting an entry the
loader would never load. Split the sets: GLOBAL_CONFIG_FILENAMES carries
config.json, project candidates do not. Regression test asserts the global
legacy file heals while a project-level config.json is left byte-identical.
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

cubic's follow-up P2 was correct and is fixed in 42311f2: the config loader merges config.json only from the global config dir, so it's now in a global-only candidate list (GLOBAL_CONFIG_FILENAMES) — project-side discovery and resolveConfigPath write-target selection no longer see an unrelated project config.json. Regression test covers both sides: the global legacy file heals, a project-level config.json stays byte-identical.

ralphstodomingo added 2 commits August 8, 2026 13:17
The adversarial guard asserted the single-path read line verbatim; the
endpoint now scans every config file the heal covers. The guarded contract —
stale-singleton bypass via readMcpEntryFromDisk + MCP.add — is unchanged and
still asserted.
The extension blanks the datamate entry to {} (not delete) in non-active-IDE
mcp.json files, and the sorted scan can reach the blanked file first (.cursor/
sorts before .vscode/). For the transport read that shadowed the real entry
behind the bare-marker fallback; for the sync it silently skipped the heal
entirely ({} has no updatedAt). Empty entries are tombstones, not transports —
both selection loops now skip them so the active IDE's real entry wins.
Comment thread packages/opencode/src/altimate/datamate-transport.ts Outdated
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

Re-verified from scratch (2026-08-25) on a pristine code-server harness (fresh mcp.json written by the extension, no prior altimate-code config), Electron contract simulated by the fake-electron shim, every scenario a real run session:

Scenario Published 0.8.10 This PR (9b1673481)
datamate_manager add file pops, -32000 Connection closed, env-less entry persisted no pop, connected as 'datamate', entry carries environment
Plain launch on the persisted entry pops
Broken project entry, plain launch healed before connect, no pop
Broken global-only entry, plain launch healed, no pop
Blanked .cursor/mcp.json tombstone sorting first healed, no pop
Published 0.8.10 launched on a healed entry no pop

That last row matters for anyone re-testing in the field: the heal is one-way, so once a machine has run this build (or re-added the entry with env), the unfixed binary stops reproducing too. A reporter who "can no longer reproduce" after testing the fix build is the expected outcome, not a sign the bug vanished — the bug reproduces deterministically on any pristine 0.8.10 install.

…cans

The transport read and the sync source selection each open-coded the
"missing or empty datamate entry" check. One isBlankDatamateEntry helper
keeps the two scans agreeing on what a tombstone is.
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

@codex review

@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: 692417ac5f

ℹ️ 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 packages/opencode/src/altimate/datamate-transport.ts Outdated
Comment thread packages/opencode/src/server/server.ts
* spawn boots the editor GUI — which opens datamate-cli.js as a document in
* the IDE — instead of running it as a Node script.
*/
function extractSpawnEnvironment(raw: unknown): Record<string, string> | undefined {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

CRITICAL — Security: a repo-local mcp.json can rewrite the global config's command + env, automatically, at every boot

extractSpawnEnvironment copies every string-valued key from the IDE entry's env, stripping only ALTIMATE_EXTENSION_RPC. Three mechanisms compound into a new trust-boundary crossing:

  1. The transport source is findAllMcpJsonFiles()Glob.scan("**/mcp.json") over the whole project tree (line 113), not just .vscode/. Any mcp.json in a cloned repo is a candidate, selected by sort order.
  2. The heal now writes the global config (line 349, via findAllConfigPaths), so the entry it produces outlives the project.
  3. It now runs automatically at TUI/run boot (cli/tui/worker.ts:52, cli/cmd/run.ts:953), where before it required serve boot or an explicit datamate_manager add.

Net new capability: cloning an untrusted repo and opening a session is enough for a repo-controlled file to replace the global datamate entry's command, args, and env — spawned on every subsequent session, in every project. At spawn, mcp/index.ts:559 spreads ...mcp.environment after ...process.env, so the carried env also overrides altimate-code's own host environment (NODE_OPTIONS, LD_PRELOAD, DYLD_INSERT_LIBRARIES, PATH).

To be precise about the delta: the env carry itself is not new — main already had the same one-key denylist in the sync path. What this PR adds is env carry on the read/handleAdd path, boot-time triggering, and global-config scope. The rating rests on the latter two.

Suggested fix — two parts, both needed:

const SPAWN_ENV_ALLOWLIST = new Set(["ELECTRON_RUN_AS_NODE"])
for (const [key, value] of Object.entries(raw as Record<string, unknown>)) {
  if (!SPAWN_ENV_ALLOWLIST.has(key)) continue
  if (typeof value === "string") env[key] = value
}

Second, bind provenance before writing: only auto-heal from an mcp.json that is the known extension-written location for this workspace, and never promote a project-file-derived transport into the global config without explicit user action.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed on all three mechanisms and the rating — thank you for the precise delta. Addressed in e96f26b, both parts:

Env: extractSpawnEnvironment is now an allowlist of exactly ELECTRON_RUN_AS_NODE (the extension writes nothing else we need); NODE_OPTIONS/LD_PRELOAD/PATH/etc. are dropped regardless of the file. Test: "only ELECTRON_RUN_AS_NODE is carried…".

Provenance: the transport source is now only the two locations the extension writes — **/.vscode/mcp.json and **/.cursor/mcp.json (not **/mcp.json), and every entry altimate-code derives from an IDE file is stamped managedBy: "altimate-ide" + sourceMcpJson. The boot heal rewrites project-scope entries as before, but touches a global entry only when its stamp matches the exact IDE file in hand; hand-added and legacy global entries are never rewritten from a project file — the explicit datamate_manager add is what (re)stamps them. Tests: hand-added global survives byte-identical; global managed from a different project's file is left alone; docs/examples/mcp.json is never a source. Re-verified end-to-end in the code-server harness (clone-shaped project file can no longer reach the global config at boot).

* a session launched from a subdirectory would otherwise scan the subtree and
* miss both the IDE config and the persisted entry it needs to repair.
*/
export async function resolveDatamateSyncRoot(directory: string): Promise<string> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

MAJOR — Logic Error: unbounded walk to / can heal from an unrelated project's mcp.json

Filesystem.up({ targets: [".git"], start: directory }) passes no stop, and the helper only breaks on stop === current (util/filesystem.ts:324). From a directory with no .git ancestor the walk reaches /. If any ancestor is a git repo — a ~ under dotfiles management is the common case — that ancestor becomes the "project root". findAllMcpJsonFiles then globs its entire tree and takes the first non-blank datamate entry by sort order, which can be a different project's entry. Its command and env are then written into this project's and the global config.

The fix already has in-repo precedent at config/paths.ts:46-48:

Filesystem.up({ targets: [".git"], start: directory, stop: Global.Path.home })

Separately: .git is a file in worktrees and submodules, so the current code resolves to the submodule root and misses the superproject's IDE config. Neither case is covered by the new tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e96f26b — the walk passes stop: Global.Path.home, and because Filesystem.up's stop is inclusive (the stop dir is still searched), a .git found at home is explicitly rejected so a dotfiles-managed ~ is never treated as a project. On .git files: the nearest one (worktree/submodule root) wins, which matches how Project.fromDirectory derives the sandbox, so altimate-code's own notion of the project and the heal's stay consistent — kept deliberately, now documented and covered. Tests added for both: home-as-git-repo → falls back to the launch dir; .git file → nearest root.

}

let datamateHealed = false
for (const configPath of await findAllConfigPaths(cwd, globalConfigDir)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

MAJOR — Logic Error: the heal rewrites the global config from a project-local IDE entry

This loop writes the transport derived from this project's mcp.json into every config carrying a datamate entry, including ~/.config/altimate-code/altimate-code.json.

Concretely: a user runs datamate_manager add --scope global (remote/cloud transport, no IDE involved). They later open any project that has a .vscode/mcp.json with a desktop stdio entry. The global entry is silently replaced by that project's Electron command + env. Every later session in every other project now spawns that binary. addMcpToConfig overwrites in place, so there is no undo.

The PR description justifies this with "a stale global entry pops the file just the same", which is true — but the remedy overreaches from "refresh an entry the IDE owns" to "replace any global entry from any project file".

Suggested fix: do not gate on "has updatedAt" — that is not evidence an entry is extension-managed, and an unrelated project or an attacker can supply one. Use explicit managed-provenance metadata bound to the workspace/source, or do not auto-rewrite the global config from project files at all.

Worth a test either way: a hand-added global entry should survive a project-local heal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — updatedAt was a change signal, not authorization, and the remedy overreached. Fixed in e96f26b with explicit provenance: entries written from an IDE transport carry managedBy + sourceMcpJson (stamped by datamate_manager add and by the sync itself); the boot heal only rewrites a global entry whose stamp matches the exact mcp.json it is healing from, and never creates one. Your scenario — add --scope global with a remote transport, then opening a project with a desktop stdio entry — now leaves the global entry byte-identical (test: "a hand-added GLOBAL entry (no provenance) survives a project-local heal byte-identical"; plus "managed from a DIFFERENT project's mcp.json is left alone"). Trade-off made explicit in the PR body: a legacy env-less global entry from 0.8.10 no longer self-heals at boot; one explicit add restamps and heals it (verified E2E).

* active IDE's real entry is shadowed by whichever file sorts first
* (`.cursor/` sorts before `.vscode/`).
*/
function isBlankDatamateEntry(entry: unknown): boolean {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

MAJOR — Bug: a non-empty but invalid IDE entry is selected as authoritative and writes a remote entry with no url

isBlankDatamateEntry only rejects {}. An entry like:

{ "servers": { "datamate": { "type": "stdio", "updatedAt": "T2" } } }

has a key, so it passes the blank check and wins source selection over a valid entry in a file that sorts later. It then reaches the sync's transport branch, where "command" in datamateVscode is false, so it falls to the else (remote) branch and writes:

{ "type": "remote", "url": undefined, "updatedAt": "T2" }

addMcpToConfig validates JSON syntax, not the MCP schema, so the malformed entry persists and breaks config loading on the next read. The bad entry also shadows any valid datamate entry that sorts after it.

Suggested fix: validate before selecting a source — local requires a non-empty string command, remote requires a non-empty url. Reject incomplete entries and continue scanning later files. Replacing "command" in datamateVscode with a validated discriminated transport would close this at the same time.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e96f26b exactly as suggested — source selection now goes through a validating parseIdeTransport: remote requires a non-empty string url, local a non-empty string command; blank tombstones and incomplete entries return null and scanning continues to later files. The "command" in datamateVscode branch is gone — the sync consumes the validated discriminated transport, so a url-less remote can no longer be written. Tests: the incomplete-entry-sorts-first case (valid later file wins, persisted entry has no url key), and a lone incomplete entry writes nothing. The PR893 "marker fallback" test was updated to assert null, with the rationale in the test.

@sahrizvi

Copy link
Copy Markdown
Contributor

Consensus review — remaining findings (minor / nit / cross-cutting)

Verdict: request changes — 1 critical, 3 major, 12 minor, 4 nits. The critical and majors are posted as inline comments on datamate-transport.ts. Everything below either spans multiple files or is below the inline bar.

The headline fix is correct and worth landing: carrying the IDE entry's env so an Electron-binary command actually runs as Node. extractSpawnEnvironment and TRANSPORT_IDENTITY_FIELDS genuinely eliminate the two-sets-can-drift bug class, the {}-tombstone fix is subtle and correct, per-file heal isolation is well judged, and the 16-case test file is thorough. The risk is in the breadth — a serve-only heal became unconditional on every TUI session and every run, with global-config write scope.

Minor

m1. updatedAt is the sole change signaldatamate-transport.ts:271
if (datamateVscode && vscodeUpdatedAt) gates the whole heal, and the inner check skips on timestamp equality without comparing command, args, url, or environment. An IDE entry with no updatedAt is never healed; one whose timestamp already matches is never healed even if its persisted environment is missing. Entries broken by 0.8.10 do heal (they have no updatedAt), which is why this is minor rather than major — but the signal should be content, with the timestamp as an optimization.

m2. reload-datamate picks the first matching file, not the effective oneserver.ts:695-703
findAllConfigPaths scan order is not the config loader's merge precedence, and preserved fields (timeout, headers, oauth, enabled) may differ between healed files — so MCP.add can connect a lower-precedence entry. MCP.add is also called regardless of enabled: false. Returning the healed {name, configPath} pairs from the sync would be more direct than re-deriving by search order.

m3. handleAdd's refresh merges remote-only fields into a local entrytools/datamate.ts:281-292
preserved excludes TRANSPORT_IDENTITY_FIELDS + enabled, but headers, headersCommand and oauth are in neither, and per core/src/v1/config/mcp.ts those exist only on Remote. If the persisted entry was type: "remote" (the cloud path) and the IDE now offers stdio, refreshed becomes { headers, oauth, type: "local", command, environment, enabled } — written to disk and passed to MCP.add. The as Parameters<...> casts are the only reason it compiles. Same defect in the sync heal path (datamate-transport.ts:305-322). Excess keys are ignored at decode, so this is disk junk and a type-safety hole rather than a runtime break — but dropping the casts would let the compiler catch it.

m4. The boot heal runs an unpruned **/mcp.json walkcli/tui/worker.ts:52, cli/cmd/run.ts:952
Glob.Options has no ignore field (core/src/util/glob.ts:5-11), so node_modules/dist/.git are fully traversed and the ignoredDirs list filters results afterward. Measured on this repo: 178ms unpruned vs 9ms pruned (~20×). worker.ts gates rpc.fetch, rpc.server and traceReady on it; run.ts awaits it before bootstrap() on every invocation including scripted ones. Cheap fix: skip the recursive scan when neither <root>/.vscode/mcp.json nor <root>/.cursor/mcp.json exists.

m5. Persisted environment is re-substituted at every config loaddatamate-transport.ts:322
The discover path resolves ${VAR} in-memory and never persists. This PR persists the raw IDE env block, which then passes through ConfigVariable.substitute on every load — config/paths.ts:264 applies substitution to raw JSON text before parsing. A carried value containing $ gets expanded or mangled. No impact on ELECTRON_RUN_AS_NODE=1, but it is an asymmetry with the discover path, and mcp/index.ts:554-559 already warns about this class of double-resolution (citing the PR #666 review).

m6. updatedAt reaches MCP.add despite being documented as disk-onlytools/datamate.ts:292
The comment at :227-229 states the runtime schema has no such field, yet the refresh path passes refreshed (which contains updatedAt) into MCP.add. The sibling "new entry" branch correctly passes the bare mcpConfig (:305). Strip it before the call so the stated invariant holds.

m7. updatedAt is persisted and relied on but undeclared in ConfigMCPV1core/src/v1/config/mcp.ts
environment is declared; updatedAt is not. Declaring it optional on Local and Remote (annotated as a sync-internal change signal) would make the on-disk contract explicit rather than comment-enforced.

m8. handleAdd resolves one config path but the entry may live in anothertools/datamate.ts:224
resolveConfigPath returns the first existing candidate, so listMcpInConfig and readMcpEntryFromDisk see only that file. If the live datamate entry is in a different loader-merged file, handleAdd takes the "not in config yet" branch and writes a second entry, leaving merge order to decide which wins. findAllConfigPaths is used everywhere else in this PR.

m9. Non-atomic read-modify-write, now with more boot callersmcp/config.ts:44-71
addMcpToConfig reads, modifies and writes non-atomically. The PR addresses the load race but not the write race: two concurrent session starts in one project (two terminals, or TUI + run) can interleave, and a torn write is then refused by the strict-parse guard on the next boot. Temp-file + rename, or a per-file lock.

m10. findAllConfigPaths branches on dir === globalDir reference equalitymcp/config.ts:107-112
findAllConfigPaths(x, x) makes both iterations take the global branch and can return the same path twice. An explicit descriptor list ([{dir, names, subdirs}, …]) would be sturdier.

m11. The adversarial test asserts on source text, not behaviortest/upstream/adversarial/upi-config-mcp.test.ts:200-205
expect(serverSource).toContain("const configPaths = await findAllConfigPaths(directory, Global.Path.config)") passes even if the runtime behavior is wrong, and breaks on a rename or reformat. A behavior test would be: seed a global-only datamate entry, hit /altimate/mcp/reload-datamate, assert MCP.add receives the global file's entry. (This extends a pre-existing pattern rather than introducing it.)

m12. reload-datamate re-reads every config file per updated nameserver.ts:696-703
The configPaths scan is hoisted, but the inner readMcpEntryFromDisk loop re-reads and re-parses each file for every name in updatedNames. Parse once, then look names up in the parsed maps.

Nits

  • n1. server.ts:37-38readMcpEntryFromDisk and findAllConfigPaths are imported on two consecutive lines from "../mcp/config". Combine.
  • n2. run.ts:951 uses a dynamic import() while worker.ts uses a static one. If deliberate (cold-start cost on the early-return path), the comment should say so.
  • n3. isBlankDatamateEntry's doc says "missing or empty", but it also returns true for any non-object (string, number, array). That is the right behavior — the comment should describe it.
  • n4. datamateSyncReady's .catch(() => {}) is correct for "must never block the TUI", but a permanently failing heal is invisible at the call site. Logging the rejection at warn would distinguish it from "sync ran, nothing to do".

Missing tests

  • handleAdd's refresh path is entirely uncovered — the largest behavior change in datamate.ts. Untested: preserved-field carry-over, the enabled: true rewrite, and the remote→local transition in m3.
  • No test that a non-IDE-derived global entry survives a project-local heal — the current "heals a datamate entry living only in the GLOBAL config" test asserts the behavior in question.
  • No test that a malformed non-empty IDE entry is skipped in favor of a later valid one.
  • No test for an env allowlist; "non-string env values are ignored" proves filtering exists, not that it is safe.
  • No resolveDatamateSyncRoot coverage for .git-as-file (worktree/submodule) or ancestor-is-a-git-repo.
  • No test that a missing or unchanged updatedAt with a diverging environment still heals.
  • No test that reload-datamate picks the healed file, or honors enabled: false.
  • No test that a $-bearing env value survives the persist→load round-trip unmodified.

What's done well

  • extractSpawnEnvironment and TRANSPORT_IDENTITY_FIELDS are applied identically in the read and sync paths — no divergence possible.
  • The {}-tombstone fix is a real bug caught: .cursor/ sorting before .vscode/ was shadowing the active IDE's entry. Two tests cover it.
  • Per-file try/catch isolation in the heal loop, with a malformed-file test, composes correctly with addMcpToConfig's strict-parse guard.
  • The getNodeValue comments explaining why a manual children[1].value walk drops object/array fields are the kind of "why" comment that pays for itself.
  • Test isolation improved — the injectable global dir keeps tests off the real ~/.config/altimate-code.

…auto-rewrite a global entry from a project file

Review on this PR showed the boot-time heal plus the global-config heal had
turned the pre-existing "scan any mcp.json in the tree" transport source into
a trust-boundary crossing: a repo-local file could replace the global
datamate entry's command/args/env automatically at every session start, and
the carried env overrode the host process env at spawn.

- Env carry is an allowlist (ELECTRON_RUN_AS_NODE only), not a denylist.
- Transport sources are only the two locations the extension writes
  (**/.vscode/mcp.json, **/.cursor/mcp.json), parsed through a validating
  parseIdeTransport: local needs a non-empty command, remote a non-empty url;
  blank tombstones and incomplete entries are skipped instead of winning
  selection (an incomplete entry used to persist as a url-less remote entry).
  The old bare-marker fallback is gone.
- Entries derived from an IDE file carry provenance (managedBy +
  sourceMcpJson). The boot heal rewrites project-scope entries as before but
  touches a GLOBAL entry only when its stamp matches the exact IDE file in
  hand; hand-added and legacy global entries are left alone, and an explicit
  datamate_manager add is what (re)stamps them.
- The project-root walk stops at the home directory, and a .git at home is
  not a project; .git files (worktrees/submodules) resolve to the nearest
  root as in Project.fromDirectory.
- Config heal candidates are every directory from the launch dir up to the
  root (plus the global dir under the provenance rule), matching the loader's
  upward walk so a nested package's own config is healed too.
- mcp/config exposes findProjectConfigPaths/findGlobalConfigPaths.

PR893 regression tests updated to the new contract (IDE-only locations,
required transport.source, incomplete entries rejected, allowlist env);
new tests cover each review point.
@ralphstodomingo

Copy link
Copy Markdown
Contributor Author

Re-verified end-to-end on the trust-boundary rework (e96f26b) — pristine code-server harness, extension-written mcp.json, fake-Electron shim, every row a real run session:

Scenario Result
0.8.10: add pops, -32000, env-less entry — bug intact on pristine install
0.8.10: relaunch pops
rework: add (project) no pop, connected as 'datamate', entry has environment + provenance
rework: broken project entry, relaunch healed, no pop
rework: broken global entry stamped from this project's mcp.json healed, no pop
rework: legacy/hand-added global entry (no provenance) left untouched — pops until the user acts (the boundary from the review)
rework: explicit add --scope global on it, then relaunch restamped + healed, relaunch clean
rework: blanked .cursor tombstone first healed, no pop
0.8.10 on a healed entry no pop (one-way heal)

The one behavior change vs. the previous head is deliberate and called out in the body: a legacy env-less global entry no longer self-heals at boot, because that is exactly the unsafe rewrite; one explicit add restamps it. Unit suites: 282 pass (the only failure is the pre-existing vendored-scan test that fails identically on main).

@cubic-dev-ai cubic-dev-ai 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.

2 issues found across 6 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/opencode/src/altimate/tools/datamate.ts">

<violation number="1" location="packages/opencode/src/altimate/tools/datamate.ts:236">
P2: When `datamate_manager add` finds a connected global entry without provenance, this new stamp is never persisted because the connected branch returns first. Boot healing then rejects the entry, so legacy/global entries cannot be repaired; persist the stamp before returning without replacing the live client.</violation>
</file>

<file name="packages/opencode/src/altimate/datamate-transport.ts">

<violation number="1" location="packages/opencode/src/altimate/datamate-transport.ts:116">
P2: When the launch directory reaches `$HOME` through a symlink or different Windows casing, this string comparison accepts `$HOME` as the project root. The boot scan can then inspect unrelated home projects and heal their configs; canonicalize the paths before applying the home-root rejection.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

// file. The boot-time heal rewrites a GLOBAL entry only when this stamp
// matches, so an explicit `add` is what authorizes future auto-repair of
// a global-scope entry.
const provenanceFields = { managedBy: DATAMATE_PROVENANCE, sourceMcpJson: transport.source }

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: When datamate_manager add finds a connected global entry without provenance, this new stamp is never persisted because the connected branch returns first. Boot healing then rejects the entry, so legacy/global entries cannot be repaired; persist the stamp before returning without replacing the live client.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/altimate/tools/datamate.ts, line 236:

<comment>When `datamate_manager add` finds a connected global entry without provenance, this new stamp is never persisted because the connected branch returns first. Boot healing then rejects the entry, so legacy/global entries cannot be repaired; persist the stamp before returning without replacing the live client.</comment>

<file context>
@@ -229,6 +229,11 @@ async function handleAdd(args: { datamate_id?: string; name?: string; scope?: "p
+      // file. The boot-time heal rewrites a GLOBAL entry only when this stamp
+      // matches, so an explicit `add` is what authorizes future auto-repair of
+      // a global-scope entry.
+      const provenanceFields = { managedBy: DATAMATE_PROVENANCE, sourceMcpJson: transport.source }
       const existingNames = await listMcpInConfig(configPath)
       const staleEntries = existingNames.filter(
</file context>

await matches.return()
if (dotgit) {
const root = path.dirname(dotgit)
if (root !== home) return root

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: When the launch directory reaches $HOME through a symlink or different Windows casing, this string comparison accepts $HOME as the project root. The boot scan can then inspect unrelated home projects and heal their configs; canonicalize the paths before applying the home-root rejection.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/altimate/datamate-transport.ts, line 116:

<comment>When the launch directory reaches `$HOME` through a symlink or different Windows casing, this string comparison accepts `$HOME` as the project root. The boot scan can then inspect unrelated home projects and heal their configs; canonicalize the paths before applying the home-root rejection.</comment>

<file context>
@@ -64,10 +101,20 @@ function isBlankDatamateEntry(entry: unknown): boolean {
-    if (dotgit) return path.dirname(dotgit)
+    if (dotgit) {
+      const root = path.dirname(dotgit)
+      if (root !== home) return root
+    }
   } catch {
</file context>

* vscode|cursor). Anything else in a checkout is not an extension-authored
* entry and must not become a transport source.
*/
const IDE_MCP_JSON_PATTERNS = ["**/.vscode/mcp.json", "**/.cursor/mcp.json"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

WARNING: Narrowing the transport scan to .vscode/.cursor drops .github/copilot/mcp.json, which this tool previously discovered and still documents as supported

IDE_MCP_JSON_PATTERNS replaces the prior Glob.scan('**/mcp.json'), which also reached .github/copilot/mcp.json. The repo's own mcp/discover.ts precedence list (IDE_PRECEDENCE = ['.vscode/mcp.json', '.cursor/mcp.json', '.github/copilot/mcp.json']) and the datamate manager's doc comment (altimate/tools/datamate.ts:33 - 'Scans .vscode/mcp.json, .cursor/mcp.json, .github/copilot/mcp.json ... so this works in Cursor, Copilot, and other IDEs') both treat the Copilot location as first-class. After this change both readDatamateTransportFromIde and syncDatamateUrlFromVscodeMcp silently stop detecting a datamate entry written to .github/copilot/mcp.json, regressing Copilot users. If the exclusion is deliberate, the stale datamate.ts comment should be updated; otherwise add '**/.github/copilot/mcp.json' to the list.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

datamate-cli.js opens as an editor tab when launching sessions (stdio MCP spawn loses ELECTRON_RUN_AS_NODE)

3 participants