mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[codex] Support npm marketplace plugin sources (#29375)
## Why
Marketplace source deserialization treated `{"source":"npm", ...}` as
unsupported. The loader logged and skipped the entry, so npm-backed
plugins never appeared in `plugin list --available` and `plugin add`
returned "plugin not found".
Codex plugins are installed from a plugin root, not from an npm
dependency tree. For npm-backed marketplace entries, Codex should fetch
the published package contents without running package scripts or
installing unrelated dependencies.
## What changed
- Add `npm` marketplace plugin sources with `package`, optional semver
`version` or version range, and optional HTTPS `registry`.
- Reject unsafe npm source fields before materialization, including
invalid package names, non-semver version selectors, plaintext or
credential-bearing registry URLs, and registry query/fragment data.
- Materialize npm plugins with `npm pack --ignore-scripts`, then unpack
the resulting tarball through the existing hardened plugin bundle
extractor.
- Enforce npm archive and extracted-size limits, require the standard
npm `package/` archive root, and verify the extracted `package.json`
name matches the requested package before installing.
- Keep plugin listings, install-source descriptions, CLI JSON/human
output, app-server v2 `PluginSource`, TUI source summaries, regenerated
schema fixtures, and app-server documentation in sync.
## Impact
Marketplaces can distribute Codex plugins from public or configured
private HTTPS npm registries using the same install flow as existing
materialized plugin sources. `npm` must be available on `PATH` when an
npm-backed plugin is installed.
Fixes #27831
## Validation
- `just write-app-server-schema`
- `just test -p codex-core-plugins -p codex-app-server-protocol -p
codex-app-server -p codex-cli`
- npm/schema/core-plugin coverage passed in the run.
- The full focused command finished with `1739 passed`, `11 failed`, and
`6 timed out`; the failures were unrelated local app-server environment
failures from `sandbox-exec: sandbox_apply: Operation not permitted`
plus one missing `test_stdio_server` helper binary.
- Installed an npm-published Codex plugin package through a throwaway
local marketplace and throwaway `CODEX_HOME` to exercise the real npm
materialization path end to end.
This commit is contained in:
+34
@@ -14207,6 +14207,40 @@
|
||||
"title": "GitPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"registry": {
|
||||
"description": "Optional HTTPS registry URL. Authentication stays in the user's npm config.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"npm"
|
||||
],
|
||||
"title": "NpmPluginSourceType",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "Optional npm version or version range.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"package",
|
||||
"type"
|
||||
],
|
||||
"title": "NpmPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
|
||||
"properties": {
|
||||
|
||||
+34
@@ -10611,6 +10611,40 @@
|
||||
"title": "GitPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"registry": {
|
||||
"description": "Optional HTTPS registry URL. Authentication stays in the user's npm config.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"npm"
|
||||
],
|
||||
"title": "NpmPluginSourceType",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "Optional npm version or version range.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"package",
|
||||
"type"
|
||||
],
|
||||
"title": "NpmPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
|
||||
"properties": {
|
||||
|
||||
@@ -413,6 +413,40 @@
|
||||
"title": "GitPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"registry": {
|
||||
"description": "Optional HTTPS registry URL. Authentication stays in the user's npm config.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"npm"
|
||||
],
|
||||
"title": "NpmPluginSourceType",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "Optional npm version or version range.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"package",
|
||||
"type"
|
||||
],
|
||||
"title": "NpmPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
|
||||
"properties": {
|
||||
|
||||
@@ -413,6 +413,40 @@
|
||||
"title": "GitPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"registry": {
|
||||
"description": "Optional HTTPS registry URL. Authentication stays in the user's npm config.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"npm"
|
||||
],
|
||||
"title": "NpmPluginSourceType",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "Optional npm version or version range.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"package",
|
||||
"type"
|
||||
],
|
||||
"title": "NpmPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
|
||||
"properties": {
|
||||
|
||||
@@ -553,6 +553,40 @@
|
||||
"title": "GitPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"registry": {
|
||||
"description": "Optional HTTPS registry URL. Authentication stays in the user's npm config.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"npm"
|
||||
],
|
||||
"title": "NpmPluginSourceType",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "Optional npm version or version range.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"package",
|
||||
"type"
|
||||
],
|
||||
"title": "NpmPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
|
||||
"properties": {
|
||||
|
||||
@@ -369,6 +369,40 @@
|
||||
"title": "GitPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"registry": {
|
||||
"description": "Optional HTTPS registry URL. Authentication stays in the user's npm config.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"npm"
|
||||
],
|
||||
"title": "NpmPluginSourceType",
|
||||
"type": "string"
|
||||
},
|
||||
"version": {
|
||||
"description": "Optional npm version or version range.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"package",
|
||||
"type"
|
||||
],
|
||||
"title": "NpmPluginSource",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"description": "The plugin is available in the remote catalog. Download metadata is kept server-side and is not exposed through the app-server API.",
|
||||
"properties": {
|
||||
|
||||
@@ -3,4 +3,12 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
|
||||
export type PluginSource = { "type": "local", path: AbsolutePathBuf, } | { "type": "git", url: string, path: string | null, refName: string | null, sha: string | null, } | { "type": "remote" };
|
||||
export type PluginSource = { "type": "local", path: AbsolutePathBuf, } | { "type": "git", url: string, path: string | null, refName: string | null, sha: string | null, } | { "type": "npm", package: string,
|
||||
/**
|
||||
* Optional npm version or version range.
|
||||
*/
|
||||
version: string | null,
|
||||
/**
|
||||
* Optional HTTPS registry URL. Authentication stays in the user's npm config.
|
||||
*/
|
||||
registry: string | null, } | { "type": "remote" };
|
||||
|
||||
@@ -744,6 +744,15 @@ pub enum PluginSource {
|
||||
ref_name: Option<String>,
|
||||
sha: Option<String>,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Npm {
|
||||
package: String,
|
||||
/// Optional npm version or version range.
|
||||
version: Option<String>,
|
||||
/// Optional HTTPS registry URL. Authentication stays in the user's npm config.
|
||||
registry: Option<String>,
|
||||
},
|
||||
/// The plugin is available in the remote catalog. Download metadata is
|
||||
/// kept server-side and is not exposed through the app-server API.
|
||||
Remote,
|
||||
|
||||
@@ -2927,7 +2927,7 @@ fn skills_extra_roots_set_params_rejects_relative_roots() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_source_serializes_local_git_and_remote_variants() {
|
||||
fn plugin_source_serializes_local_git_npm_and_remote_variants() {
|
||||
let local_path = if cfg!(windows) {
|
||||
r"C:\plugins\linear"
|
||||
} else {
|
||||
@@ -2961,6 +2961,21 @@ fn plugin_source_serializes_local_git_and_remote_variants() {
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(PluginSource::Npm {
|
||||
package: "@acme/plugin".to_string(),
|
||||
version: Some("^1.2.0".to_string()),
|
||||
registry: Some("https://npm.example.com".to_string()),
|
||||
})
|
||||
.unwrap(),
|
||||
json!({
|
||||
"type": "npm",
|
||||
"package": "@acme/plugin",
|
||||
"version": "^1.2.0",
|
||||
"registry": "https://npm.example.com",
|
||||
}),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
serde_json::to_value(PluginSource::Remote).unwrap(),
|
||||
json!({
|
||||
|
||||
Reference in New Issue
Block a user