Skip to content

fix: Generate desktop runtime core lock#125

Merged
zouyonghe merged 6 commits intomainfrom
codex/desktop-plugin-dependency-lock
Apr 28, 2026
Merged

fix: Generate desktop runtime core lock#125
zouyonghe merged 6 commits intomainfrom
codex/desktop-plugin-dependency-lock

Conversation

@zouyonghe
Copy link
Copy Markdown
Member

@zouyonghe zouyonghe commented Apr 28, 2026

Summary

Generates a runtime-core-lock.json file during packaged backend resource preparation and exposes it to the bundled backend through ASTRBOT_DESKTOP_CORE_LOCK_PATH. The lock records installed runtime distributions and top-level modules so AstrBot can protect the packaged backend from plugin dependency replacement.

Why

Desktop plugin installs write into ASTRBOT_ROOT/data/site-packages. Without a packaged runtime lock, a plugin can install another copy or version of packages already used by the backend runtime, then the live process may attempt to prefer those modules. That caused Cua installation to trip OpenAI/Pydantic/module identity conflicts until restart.

Related

Tests

  • node --test scripts/backend/runtime-core-lock.test.mjs
  • pnpm run test:prepare-resources
  • python3 -m py_compile scripts/backend/tools/generate_runtime_core_lock.py

Summary by Sourcery

Generate a desktop backend runtime core lock and wire it into the packaged backend launch flow to protect bundled runtime dependencies from being overridden by plugin installs.

New Features:

  • Produce a runtime core lock JSON file during backend resource preparation that records installed distributions and their top-level modules.
  • Expose the runtime core lock file path to the bundled backend via an environment variable when the lock is present.

Documentation:

  • Add design and implementation plan documents describing the desktop plugin dependency boundary and runtime core lock behavior.

Tests:

  • Add Node-based tests for runtime core lock generation, error handling, and launcher environment wiring.

@zouyonghe zouyonghe changed the title [codex] Generate desktop runtime core lock fix: Generate desktop runtime core lock Apr 28, 2026
Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • In launch_backend.py, you define RUNTIME_CORE_LOCK_ENV but still hard-code the environment variable name in configure_runtime_core_lock_path; consider using the constant there to avoid divergence if the name ever changes.
  • In generate_runtime_core_lock.py, _read_top_level_modules catches a broad Exception and silently returns an empty list; narrowing this to the expected FileNotFoundError/KeyError (or at least logging unexpected errors) would make failures around malformed top_level.txt files more observable.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `launch_backend.py`, you define `RUNTIME_CORE_LOCK_ENV` but still hard-code the environment variable name in `configure_runtime_core_lock_path`; consider using the constant there to avoid divergence if the name ever changes.
- In `generate_runtime_core_lock.py`, `_read_top_level_modules` catches a broad `Exception` and silently returns an empty list; narrowing this to the expected `FileNotFoundError`/`KeyError` (or at least logging unexpected errors) would make failures around malformed `top_level.txt` files more observable.

## Individual Comments

### Comment 1
<location path="scripts/backend/templates/launch_backend.py" line_range="227" />
<code_context>
+def configure_runtime_core_lock_path() -> None:
+    lock_path = APP_DIR / "runtime-core-lock.json"
+    if lock_path.is_file():
+        os.environ["ASTRBOT_DESKTOP_CORE_LOCK_PATH"] = str(lock_path)
+
+
</code_context>
<issue_to_address>
**suggestion:** Reuse the RUNTIME_CORE_LOCK_ENV constant instead of hardcoding the environment variable name.

Since `RUNTIME_CORE_LOCK_ENV = "ASTRBOT_DESKTOP_CORE_LOCK_PATH"` is already defined, use that constant here (and anywhere else this env var is used) to keep the name centralized and avoid mismatches if it changes later.

Suggested implementation:

```python
        os.environ[RUNTIME_CORE_LOCK_ENV] = str(lock_path)

```

Search the rest of `scripts/backend/templates/launch_backend.py` (and the broader codebase, if appropriate) for any other occurrences of the string literal `"ASTRBOT_DESKTOP_CORE_LOCK_PATH"` and replace them with `RUNTIME_CORE_LOCK_ENV` in the same way, so the environment variable name remains centralized in one constant.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/backend/templates/launch_backend.py Outdated
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request implements a dependency boundary for desktop plugins to prevent them from shadowing or conflicting with core backend runtime dependencies. It introduces a mechanism to generate a runtime-core-lock.json file during the build process, which captures installed distributions and their top-level modules. The launcher is updated to expose this lock file via an environment variable, allowing the pip installer to enforce constraints. Feedback focuses on ensuring compatibility with Python versions prior to 3.10 when accessing distribution metadata and improving code maintainability by using defined constants for environment variable keys.


def _distribution_record(distribution: importlib_metadata.Distribution) -> dict[str, object] | None:
name = distribution.metadata.get("Name")
version = distribution.version
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.

high

The Distribution.version property was introduced in Python 3.10. If the bundled runtime is Python 3.8 or 3.9, this will raise an AttributeError. To ensure compatibility with all versions that include importlib.metadata, you should retrieve the version from the metadata headers, similar to how the name is retrieved at line 24.

Suggested change
version = distribution.version
version = distribution.metadata.get("Version")

def configure_runtime_core_lock_path() -> None:
lock_path = APP_DIR / "runtime-core-lock.json"
if lock_path.is_file():
os.environ["ASTRBOT_DESKTOP_CORE_LOCK_PATH"] = str(lock_path)
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.

medium

The environment variable name is already defined as a constant RUNTIME_CORE_LOCK_ENV at line 18. It is better to use the constant here to maintain consistency and avoid potential typos.

Suggested change
os.environ["ASTRBOT_DESKTOP_CORE_LOCK_PATH"] = str(lock_path)
os.environ[RUNTIME_CORE_LOCK_ENV] = str(lock_path)

Reuse the launcher env constant and only suppress missing top_level.txt metadata so unexpected lock-generation failures stay visible.
@zouyonghe
Copy link
Copy Markdown
Member Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've left some high level feedback:

  • In configure_runtime_core_lock_path it might be useful to skip setting ASTRBOT_DESKTOP_CORE_LOCK_PATH when it is already present in the environment so that advanced users or tests can override the lock location if needed.
  • Both generateRuntimeCoreLock and generate_runtime_core_lock.py ensure the output directory exists; you could drop one of the mkdir calls to avoid redundant filesystem work and keep the responsibility in a single layer.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `configure_runtime_core_lock_path` it might be useful to skip setting `ASTRBOT_DESKTOP_CORE_LOCK_PATH` when it is already present in the environment so that advanced users or tests can override the lock location if needed.
- Both `generateRuntimeCoreLock` and `generate_runtime_core_lock.py` ensure the output directory exists; you could drop one of the `mkdir` calls to avoid redundant filesystem work and keep the responsibility in a single layer.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Preserve explicit launcher lock path overrides for tests and advanced setups, and let the Python lock generator own output directory creation.
@zouyonghe
Copy link
Copy Markdown
Member Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="docs/plans/2026-04-28-desktop-plugin-dependency-boundary-design.md" line_range="9-15" />
<code_context>
+shadow packages already used by the bundled backend runtime. Packages such as
+`openai`, `pydantic`, `fastapi`, `numpy`, and PyObjC modules are unsafe to hot-reload
+inside a running backend process. Installing Cua exposed this failure mode: pip
+installed a second dependency set into plugin `site-packages`, the installer tried to
+prefer those modules in-process, and the running OpenAI client later saw incompatible
+class identities.
</code_context>
<issue_to_address>
**nitpick (typo):** Consider revising this sentence to avoid the comma splice between independent clauses.

In particular, the sentence starting with “pip installed a second dependency set…” links multiple independent clauses with commas. Consider splitting it into two sentences, or use a semicolon or conjunction (e.g., “..., and the installer tried...”) to clarify the structure.

```suggestion
This allows plugin-only dependencies to work, but it also allows plugin installs to
shadow packages already used by the bundled backend runtime. Packages such as
`openai`, `pydantic`, `fastapi`, `numpy`, and PyObjC modules are unsafe to hot-reload
inside a running backend process. Installing Cua exposed this failure mode: pip
installed a second dependency set into plugin `site-packages`; the installer tried to
prefer those modules in-process, and the running OpenAI client later saw incompatible
class identities.
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread docs/plans/2026-04-28-desktop-plugin-dependency-boundary-design.md
Clarify the Cua failure-mode sentence in the desktop plugin dependency boundary design by replacing the comma splice with clearer punctuation.
@zouyonghe
Copy link
Copy Markdown
Member Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • The runtime-core-lock.test.mjs checks for implementation details via string matching (e.g., assert.doesNotMatch(source, /mkdirSync\(/)), which is brittle; consider asserting behavior via a small integration-style call or exported API semantics instead of source inspection.
  • The plan document 2026-04-28-desktop-plugin-dependency-boundary.md includes tool-specific directives (the “For Claude” note) that don’t affect the design itself; consider removing or relocating those to avoid embedding reviewer-specific instructions in long-lived docs.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `runtime-core-lock.test.mjs` checks for implementation details via string matching (e.g., `assert.doesNotMatch(source, /mkdirSync\(/)`), which is brittle; consider asserting behavior via a small integration-style call or exported API semantics instead of source inspection.
- The plan document `2026-04-28-desktop-plugin-dependency-boundary.md` includes tool-specific directives (the “For Claude” note) that don’t affect the design itself; consider removing or relocating those to avoid embedding reviewer-specific instructions in long-lived docs.

## Individual Comments

### Comment 1
<location path="scripts/backend/runtime-core-lock.mjs" line_range="9-18" />
<code_context>
+export const generateRuntimeCoreLock = ({ runtimePython, outputPath }) => {
</code_context>
<issue_to_address>
**suggestion:** Enrich error messages with the Python executable and generator script path to make diagnosing failures easier.

The error messages (`Failed to generate runtime core lock`, `Runtime core lock generation failed`, `did not create ...`) don’t indicate which Python binary or script were used. Please include `runtimePython.absolute` and `generatorScriptPath` in these messages (e.g. ` (python: ${runtimePython.absolute}, script: ${generatorScriptPath})`) to make misconfigurations easier to diagnose.

Suggested implementation:

```javascript
  if (!runtimePython?.absolute) {
    throw new Error(
      `Missing runtime Python executable for runtime core lock generation (python: ${
        runtimePython?.absolute ?? 'undefined'
      }, script: ${generatorScriptPath}).`,
    );
  }

```

```javascript
  if (!outputPath) {
    throw new Error(
      `Missing output path for runtime core lock generation (python: ${runtimePython.absolute}, script: ${generatorScriptPath}).`,
    );
  }

```

To fully implement your comment, the following additional updates are needed elsewhere in `scripts/backend/runtime-core-lock.mjs` (not visible in the provided snippet):

1. Locate all occurrences of the messages:
   - `Failed to generate runtime core lock`
   - `Runtime core lock generation failed`
   - `did not create ...` (likely something like `did not create ${outputPath}` or similar)
2. For each of these, wrap the message in a template literal and append diagnostic context, for example:
   - `Failed to generate runtime core lock (python: ${runtimePython.absolute}, script: ${generatorScriptPath}).`
   - `Runtime core lock generation failed (python: ${runtimePython.absolute}, script: ${generatorScriptPath}).`
   - `did not create ${outputPath} (python: ${runtimePython.absolute}, script: ${generatorScriptPath}).`
3. If any of those messages are emitted outside the scope where `runtimePython` is directly available, you will need to either:
   - Pass `runtimePython` (or at least `runtimePython.absolute`) down into the relevant function(s), or
   - Capture the values in a higher scope that these log/error statements can access.

These changes will ensure all relevant error and diagnostic messages contain both the Python executable path and the generator script path, as requested.
</issue_to_address>

### Comment 2
<location path="scripts/backend/tools/generate_runtime_core_lock.py" line_range="9-12" />
<code_context>
+from pathlib import Path
+
+
+def _read_top_level_modules(distribution: importlib_metadata.Distribution) -> list[str]:
+    try:
+        text = distribution.read_text("top_level.txt") or ""
+    except (FileNotFoundError, KeyError):
+        return []
+
</code_context>
<issue_to_address>
**suggestion:** Consider handling text decoding issues when reading top-level metadata so a single bad distribution does not break lock generation.

Currently `_read_top_level_modules` only ignores `FileNotFoundError` and `KeyError`. If `distribution.read_text("top_level.txt")` hits an unexpected encoding and raises a `UnicodeDecodeError`, lock generation will fail. Since this metadata is best-effort, consider also catching decoding-related errors (e.g. `UnicodeError`) and returning an empty list to keep generation robust against corrupted or non-standard distributions.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/backend/runtime-core-lock.mjs
Comment thread scripts/backend/tools/generate_runtime_core_lock.py Outdated
Replace brittle runtime core lock source-inspection tests with behavior checks, add generator context to failure messages, and tolerate undecodable top-level metadata. Remove the tool-specific execution note from the implementation plan doc.
@zouyonghe
Copy link
Copy Markdown
Member Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="scripts/backend/runtime-core-lock.mjs" line_range="38" />
<code_context>
+      `Failed to generate runtime core lock ${formatGeneratorContext(runtimePython.absolute)}: ${result.error.message}`,
+    );
+  }
+  if (result.status !== 0) {
+    const detail = result.stderr?.trim() || result.stdout?.trim() || `exit code ${result.status}`;
+    throw new Error(
</code_context>
<issue_to_address>
**suggestion:** Differentiate between non-zero exit codes and signal-based termination from the generator process.

With `spawnSync`, `result.status` is `null` when the child is terminated by a signal (`result.signal` is set). The current logic treats this as a non-zero exit and reports `exit code null`. You could special‑case the signal case for clearer diagnostics, e.g.:

```js
if (result.status !== 0) {
  let detail;
  if (result.signal) {
    detail = `terminated by signal ${result.signal}`;
  } else {
    detail = result.stderr?.trim() || result.stdout?.trim() || `exit code ${result.status}`;
  }
  throw new Error(
    `Runtime core lock generation failed ${formatGeneratorContext(runtimePython.absolute)}: ${detail}`,
  );
}
```

Suggested implementation:

```javascript
  if (result.error) {
    throw new Error(
      `Failed to generate runtime core lock ${formatGeneratorContext(runtimePython.absolute)}: ${result.error.message}`,
    );
  }

  if (result.status !== 0) {
    let detail;
    if (result.signal) {
      detail = `terminated by signal ${result.signal}`;
    } else {
      detail = result.stderr?.trim() || result.stdout?.trim() || `exit code ${result.status}`;
    }
    throw new Error(
      `Runtime core lock generation failed ${formatGeneratorContext(runtimePython.absolute)}: ${detail}`,
    );
  }

```

This edit assumes there was not already an `if (result.status !== 0)` block following the `if (result.error)` check. If such a block already exists, replace that existing `if (result.status !== 0)` block with the new one (from `if (result.status !== 0) {` down to the closing `}`) instead of simply inserting it, to avoid duplicate handling.
</issue_to_address>

### Comment 2
<location path="scripts/backend/runtime-core-lock.mjs" line_range="45-49" />
<code_context>
+    );
+  }
+
+  if (!fs.existsSync(outputPath)) {
+    throw new Error(
+      `Runtime core lock generator did not create ${outputPath} ${formatGeneratorContext(runtimePython.absolute)}.`,
</code_context>
<issue_to_address>
**suggestion:** Optionally validate the generated lock file contents, not just its existence.

The postcondition only checks that `outputPath` exists. A truncated or otherwise invalid file (e.g., from a partial write) would still pass. Consider also reading and `JSON.parse`-ing the file (or at least verifying it’s non-empty) and failing in the same error path to catch corruption earlier and protect downstream consumers.

```suggestion
  {
    const context = formatGeneratorContext(runtimePython.absolute);
    const baseMessage = `Runtime core lock generator did not create valid ${outputPath} ${context}.`;

    if (!fs.existsSync(outputPath)) {
      throw new Error(baseMessage);
    }

    const contents = fs.readFileSync(outputPath, "utf8");
    if (!contents.trim()) {
      throw new Error(baseMessage);
    }

    try {
      JSON.parse(contents);
    } catch {
      throw new Error(baseMessage);
    }
  }
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread scripts/backend/runtime-core-lock.mjs
Comment thread scripts/backend/runtime-core-lock.mjs Outdated
Report signal-terminated generator failures clearly and reject invalid generated lock files so broken build artifacts fail fast with actionable diagnostics.
@zouyonghe
Copy link
Copy Markdown
Member Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@zouyonghe zouyonghe merged commit d25f87e into main Apr 28, 2026
4 checks passed
@zouyonghe zouyonghe deleted the codex/desktop-plugin-dependency-lock branch April 28, 2026 13:47
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