dsh-gpt-adapter 0.2.2 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,9 +22,19 @@ Reasoning and speed presets are requests to the configured gateway, not guarante
22
22
 
23
23
  ## Compatibility and installation
24
24
 
25
- The native integration targets DSH `0.2.2-rc.1`, Cordis `4.1.0`, and the pinned adapter source hash. The development dependency uses Pi AI `1.3.0`. Installed third-party package files are never edited. The package has not been published to npm.
25
+ The native integration targets DSH `0.2.2-rc.1`, Cordis `4.1.0`, and the pinned adapter source hash. The development dependency uses Pi AI `1.3.0`. Installed third-party package files are never edited. The published npm package is `dsh-gpt-adapter@0.2.3`.
26
26
 
27
- Requires Node.js `>=24.9.0` and pnpm.
27
+ Requires Node.js `>=24.9.0` and DSH. Source builds also require pnpm.
28
+
29
+ Install the published package in the web profile:
30
+
31
+ ```sh
32
+ dsh plugin --profile web add dsh-gpt-adapter@0.2.3
33
+ ```
34
+
35
+ Restart the DSH host after installing or upgrading. Configure GPT connections under **Settings → Models**; Codex environment reuse is enabled by default. The plugin extends the existing native settings and conversation controls.
36
+
37
+ To install from source for development or verification:
28
38
 
29
39
  ```sh
30
40
  git clone https://github.com/lxjmaster/dsh-gpt-adapter.git
@@ -32,17 +42,16 @@ cd dsh-gpt-adapter
32
42
  pnpm install --frozen-lockfile
33
43
  mkdir -p artifacts
34
44
  pnpm pack --pack-destination artifacts
35
- dsh plugin --profile web add ./artifacts/dsh-gpt-adapter-0.2.2.tgz
45
+ dsh plugin --profile web add ./artifacts/dsh-gpt-adapter-0.2.3.tgz
36
46
  ```
37
47
 
38
- `pnpm pack` runs the production build. Restart the DSH host after upgrading the installed package.
48
+ `pnpm pack` runs the production build.
39
49
 
40
50
  ## Automated releases
41
51
 
42
52
  The `Release` workflow runs on pushes to `main` and manual dispatches. For a formal `X.Y.Z` version, it runs the test suite, creates `vX.Y.Z`, creates the matching GitHub Release, and publishes the package to npm. Prerelease versions are skipped, and existing tags or npm versions are reused safely.
43
53
 
44
- Add an npm access token as the repository secret `NPM_TOKEN` before the first release.
45
- Configure GPT connections as usual under **Settings → Models**. The plugin extends the existing native settings and conversation controls.
54
+ Maintainers must provide the npm access token as the repository secret `NPM_TOKEN` for this workflow.
46
55
 
47
56
  ## Codex environment reuse
48
57
 
@@ -58,12 +67,14 @@ Codex compatibility is enabled by default. The root is the plugin's `codexHome`
58
67
  | Codex skills | Discover `skills/` and `skills/.system/`; bodies load on demand |
59
68
  | Shared/project skills | Retain native discovery and include `.agents/skills` at every directory from cwd to repository root; nearer project skills take precedence |
60
69
  | Skill enablement | Honor ordered `skills.config` name/path selectors and `skills.bundled.enabled` |
61
- | Enabled plugins | Read declared skill roots and inline or file-referenced `mcpServers`; disabled plugins stay disabled |
70
+ | Enabled plugins | Support portable root `plugin.json` / `mcp.json` and legacy declarations; disabled plugins stay disabled |
62
71
 
63
72
  The compatibility layer reads project instructions through DSH's filesystem, selects candidates using Codex precedence, and replaces native messages only for covered scopes. Additional nested instruction updates remain applicable. It does not import Codex's built-in system prompt. Configured fallback filenames such as `CLAUDE.md` are used only when supplied in Codex configuration. Instructions are recorded as ordinary user context with a bounded byte budget, including an explicit zero budget; they do not override system, developer, or direct user instructions. Existing conversation history is retained when the plugin is removed.
64
73
 
65
74
  Plugin resources are resolved from the configured local marketplace, or the newest locally cached version in the matching marketplace. Nothing is downloaded automatically. Plugin skills use `<plugin>-<skill>` names to fit DSH's skill-name format and avoid cross-plugin collisions. Original skill paths and relative resources remain intact.
66
75
 
76
+ Portable Agent Plugins use fixed `skills/` and `mcp.json` components, regardless of legacy `skills` or `mcpServers` declarations. OpenAI settings come from the root manifest's `extensions.com.openai` object, including an empty object; only when absent does `.codex-plugin/plugin.json` supply the overlay. Plugin identity always comes from the root manifest.
77
+
67
78
  ## MCP compatibility
68
79
 
69
80
  The plugin automatically starts enabled MCP commands and connects configured HTTP endpoints for each Agent session. It imports `mcp_servers` and the MCP resources declared by enabled plugins. Trusted project overrides apply within that Agent's scope; concurrent sessions do not share imported tool registrations. Untrusted project configuration is skipped.
@@ -77,6 +88,14 @@ Supported configuration includes:
77
88
 
78
89
  Global server names are retained where DSH permits them. Plugin servers use a plugin prefix. Unchanged connections are preserved. Changed or removed connections are disposed before replacement. Invalid configuration retains the last working snapshot, including individually invalid server entries. With automatic refresh enabled, failed startup retries with exponential backoff for up to ten attempts; a manual refresh resets that budget. The default two-second refresh detects configuration and plugin-resource changes; native skill watchers detect skill file changes.
79
90
 
91
+ Plugin-scoped `plugins.<plugin>.mcp_servers.<server>` policies apply `enabled`, `enabled_tools`, `disabled_tools`, and per-tool disabling without overriding the plugin's executable, transport, or credentials. When plugin files are invalid, retained connections still receive current disabling and filtering restrictions; old restrictions are not relaxed until a valid declaration is available again.
92
+
93
+ Stdio servers default to their Agent's project working directory and reconnect when that directory changes. An explicit `cwd` takes precedence. Without an explicit `cwd`, relative executable paths still resolve from the config or plugin root, independently of the process working directory. Plugin paths support `${PLUGIN_ROOT}`, `${CODEX_PLUGIN_ROOT}`, and `${CLAUDE_PLUGIN_ROOT}`.
94
+
95
+ Imported MCP tools carry the current DSH session and turn in `tools/call` metadata under `_meta["x-codex-turn-metadata"]`. `session_id` is the Agent session ID; `turn_id` is `<session ID>:<turn/start event sequence>`. Calls in one turn share the same identity, new turns read a fresh boundary, and asynchronous sessions remain isolated. Diagnostic calls outside an active turn do not invent identities. These fields are transport metadata, never model-supplied tool arguments.
96
+
97
+ The path is DSH Agent → imported tool execution context → MCP `tools/call` metadata → `node_repl` → browser service. This bridge addresses `Missing required Codex turn metadata: session_id, turn_id`; the browser extension, authentication, and App-specific services must still be available on the host. Unloading the plugin removes the bridge, and ordinary DSH MCP calls do not receive these fields.
98
+
80
99
  Credentials are not copied into DSH settings, plugin caches, or this repository. Status responses contain only resource names, states, and sanitized reason codes. Missing environment variables must exist in the DSH host environment to be inherited.
81
100
 
82
101
  This is environment compatibility, not a replacement Codex runtime. Codex OAuth/keychain sessions, Codex App IPC services, hooks, app components, permission policies, and proprietary commands are not automatically reproduced. Standard MCPs using configured headers/environment can be reused; servers requiring unavailable runtime authentication or services report a connection failure. Plugin hooks/apps/commands are reported as unsupported rather than executed. DSH retains its own approval and sandbox behavior.
package/README.zh-CN.md CHANGED
@@ -22,9 +22,19 @@
22
22
 
23
23
  ## 兼容性与安装
24
24
 
25
- 原生集成面向 DSH `0.2.2-rc.1`、Cordis `4.1.0`,并校验适配器源码指纹。开发依赖固定 Pi AI `1.3.0`。插件不会修改已安装的第三方包文件,目前未发布到 npm。
25
+ 原生集成面向 DSH `0.2.2-rc.1`、Cordis `4.1.0`,并校验适配器源码指纹。开发依赖固定 Pi AI `1.3.0`。插件不会修改已安装的第三方包文件。当前 npm 发布版本为 `dsh-gpt-adapter@0.2.3`。
26
26
 
27
- 需要 Node.js `>=24.9.0` 和 pnpm。
27
+ 需要 Node.js `>=24.9.0` 和 DSH。源码构建另外需要 pnpm。
28
+
29
+ 在 `web` 配置档(profile)中安装已发布的 npm 包:
30
+
31
+ ```sh
32
+ dsh plugin --profile web add dsh-gpt-adapter@0.2.3
33
+ ```
34
+
35
+ 安装或升级后重启 DSH 宿主。在「设置 → 模型」中配置 GPT 连接;Codex 环境复用默认开启。插件扩展现有原生设置与对话控件。
36
+
37
+ 如需从源码安装进行开发或验证:
28
38
 
29
39
  ```sh
30
40
  git clone https://github.com/lxjmaster/dsh-gpt-adapter.git
@@ -32,17 +42,16 @@ cd dsh-gpt-adapter
32
42
  pnpm install --frozen-lockfile
33
43
  mkdir -p artifacts
34
44
  pnpm pack --pack-destination artifacts
35
- dsh plugin --profile web add ./artifacts/dsh-gpt-adapter-0.2.2.tgz
45
+ dsh plugin --profile web add ./artifacts/dsh-gpt-adapter-0.2.3.tgz
36
46
  ```
37
47
 
38
- `pnpm pack` 会自动执行生产构建。升级安装包后重启 DSH 宿主。
48
+ `pnpm pack` 会自动执行生产构建。
39
49
 
40
50
  ## 自动发布
41
51
 
42
52
  `Release` 工作流会在推送到 `main` 或手动运行时执行。版本号为正式的 `X.Y.Z` 时,工作流会运行测试、创建 `vX.Y.Z` 标签、创建对应的 GitHub Release,并将包发布到 npm。预发布版本会跳过;已有标签或 npm 版本会安全复用。
43
53
 
44
- 首次发布前,请在仓库 Secrets 中添加 npm 访问令牌 `NPM_TOKEN`。
45
- 在「设置 → 模型」中照常配置 GPT 连接;插件扩展现有原生设置与对话控件。
54
+ 维护者需要在仓库 Secrets 中提供 npm 访问令牌 `NPM_TOKEN`,工作流才能发布。
46
55
 
47
56
  ## 复用 Codex 环境
48
57
 
@@ -58,12 +67,14 @@ dsh plugin --profile web add ./artifacts/dsh-gpt-adapter-0.2.2.tgz
58
67
  | Codex 技能 | 发现 `skills/` 和 `skills/.system/`,正文按需加载 |
59
68
  | 共享与项目技能 | 保留原生发现机制,并补齐当前目录到仓库根每一级 `.agents/skills`;更近的项目技能优先 |
60
69
  | 技能开关 | 遵循有序的 `skills.config` 名称/路径选择器,以及 `skills.bundled.enabled` |
61
- | 已启用插件 | 读取声明的技能目录和内联或文件引用形式的 `mcpServers`;禁用插件不加载 |
70
+ | 已启用插件 | 支持新版根目录 `plugin.json`、`mcp.json` 和旧版声明;禁用插件不加载 |
62
71
 
63
72
  兼容层通过 DSH 文件系统读取项目指令,按 Codex 优先级选择候选文件,只替换已覆盖作用域内的原生指令消息;额外发现的子目录指令更新仍然保留。它不会导入 Codex 内置系统提示词。`CLAUDE.md` 等后备文件仅在 Codex 配置明确声明时使用。指令以有字节预算的普通用户上下文写入会话,明确配置的零预算也会生效;指令不覆盖系统、开发者或用户直接指令。移除插件不会删除已有会话历史。
64
73
 
65
74
  插件资源优先来自已配置的本地市场目录,否则使用匹配市场中最新的本地缓存版本,不会自动下载。插件技能使用 `<插件名>-<技能名>`,以符合 DSH 命名格式并避免跨插件冲突。技能原始路径和相对资源引用保持不变。
66
75
 
76
+ 新版 Agent Plugins 使用固定的 `skills/` 与 `mcp.json`,不受旧版 `skills`、`mcpServers` 声明改写。OpenAI 扩展优先使用根清单的 `extensions.com.openai` 对象(包括空对象),缺省时才使用 `.codex-plugin/plugin.json`;插件名称仍取自根清单。
77
+
67
78
  ## MCP 兼容
68
79
 
69
80
  插件会为每个会话代理(Agent)自动启动已启用的 MCP 命令并连接已配置的 HTTP 端点。导入来源包括 `mcp_servers` 和已启用插件声明的 MCP 资源。受信任项目的覆盖配置只在该会话代理的作用域内生效,并行会话不会共用导入工具的注册;未受信任项目的配置会跳过。
@@ -77,6 +88,14 @@ dsh plugin --profile web add ./artifacts/dsh-gpt-adapter-0.2.2.tgz
77
88
 
78
89
  全局服务器名称在 DSH 允许时原样保留,插件服务器增加插件名前缀。未变化的连接继续复用;变更或删除的连接先卸载再替换。配置损坏时保留上一份有效快照,单个服务器配置无效时也会保留其可用连接。开启自动刷新时,启动失败会按指数退避间隔重试,最多尝试十次;手动刷新会重置次数。默认每两秒检查配置与插件资源变化,技能文件变化由原生监视器处理。
79
90
 
91
+ 插件级 `plugins.<插件>.mcp_servers.<服务器>` 的 `enabled`、`enabled_tools`、`disabled_tools` 和单工具禁用设置会覆盖声明中的策略,但不能改写插件的命令、传输或凭证。插件文件损坏时,保留的旧连接仍应用最新禁用和过滤限制;有效声明恢复前,不会放宽旧快照已有的限制。
92
+
93
+ stdio 服务器默认从所属 Agent 的项目工作目录启动,切换项目时重建连接;显式 `cwd` 配置优先。未显式配置 `cwd` 时,相对可执行文件路径仍按配置或插件根目录解析,避免与运行目录混淆。插件根目录替换同时支持 `${PLUGIN_ROOT}`、`${CODEX_PLUGIN_ROOT}` 和 `${CLAUDE_PLUGIN_ROOT}`。
94
+
95
+ 导入的 MCP 工具会在 `tools/call` 的 `_meta["x-codex-turn-metadata"]` 中携带当前 DSH 会话和轮次信息。`session_id` 使用 Agent 的会话 ID,`turn_id` 使用 `<会话 ID>:<turn/start 事件序号>`;同一轮的调用保持一致,新轮次重新读取,异步并发会话相互隔离。没有活动轮次的诊断调用不生成虚假标识。这些字段不进入工具参数,也不要求模型填写。
96
+
97
+ 调用链为:DSH Agent → 导入工具的异步调用上下文 → MCP `tools/call` 元数据 → `node_repl` → 浏览器服务。此桥接修复浏览器服务因缺少 `session_id`、`turn_id` 抛出的 `Missing required Codex turn metadata`;浏览器扩展、认证和 App 专属服务仍需在宿主可用。插件卸载时撤销桥接,普通 DSH MCP 调用不注入这些字段。
98
+
80
99
  凭证不会复制到 DSH 设置、插件缓存或本仓库。状态接口只返回资源名、状态和经过脱敏的原因代码。需要继承的环境变量必须存在于 DSH 宿主环境中。
81
100
 
82
101
  本插件提供环境兼容,不会重新实现 Codex 运行时。Codex 的 OAuth/钥匙串会话、App 专属进程通信服务、Hooks、应用组件、权限策略和专用命令不会自动重现。使用配置中的请求头或环境变量认证的标准 MCP 可以复用;依赖缺失认证或运行时服务的服务器会报告连接失败。插件的 Hooks、应用和命令明确标为不支持,不会自动执行。审批和沙箱仍由 DSH 自身管理。
@@ -8,6 +8,7 @@ export type CompatibilityItem = {
8
8
  export type CodexMcpConfig = {
9
9
  serverName: string;
10
10
  source: string;
11
+ originalName?: string;
11
12
  transport: 'stdio' | 'streamable-http';
12
13
  command?: string;
13
14
  args?: string[];
@@ -47,7 +48,7 @@ export declare function projectText(path: string, fs?: ProjectFileSystem, signal
47
48
  /** Repository root to cwd; without a repository only cwd participates. */
48
49
  export declare function projectDirectories(cwd: string, fs?: ProjectFileSystem, signal?: AbortSignal): Promise<string[]>;
49
50
  export declare function readCodexConfig(home: string, cwd?: string, fs?: ProjectFileSystem, signal?: AbortSignal): Promise<JsonObject>;
50
- export declare function normalizeCodexMcp(name: string, value: unknown, source: string, base: string, environment?: NodeJS.ProcessEnv): CodexMcpConfig | undefined;
51
+ export declare function normalizeCodexMcp(name: string, value: unknown, source: string, base: string, environment?: NodeJS.ProcessEnv, defaultCwd?: string): CodexMcpConfig | undefined;
51
52
  export declare function discoverCodexSnapshot(home: string, options: {
52
53
  importPluginResources: boolean;
53
54
  mcpAllowlist: string[];
@@ -0,0 +1,12 @@
1
+ export type CodexTurnMetadata = {
2
+ session_id: string;
3
+ turn_id: string;
4
+ };
5
+ /**
6
+ * DSH 0.2.2-rc.1 has no per-call MCP metadata hook. Scope the SDK seam to
7
+ * imported tool executions so its native lifecycle and result handling remain intact.
8
+ */
9
+ export declare function installCodexMcpMetadata(): {
10
+ run<T>(metadata: CodexTurnMetadata | undefined, operation: () => T): T;
11
+ dispose(): void;
12
+ };
package/lib/index.js CHANGED
@@ -1082,7 +1082,7 @@ function serverName(name2) {
1082
1082
  const hash = createHash3("sha256").update(name2).digest("hex").slice(0, 8);
1083
1083
  return `${name2.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 23)}-${hash}`;
1084
1084
  }
1085
- function normalizeCodexMcp(name2, value, source, base, environment = process.env) {
1085
+ function normalizeCodexMcp(name2, value, source, base, environment = process.env, defaultCwd = base) {
1086
1086
  const raw = object(value);
1087
1087
  if (raw.enabled === false) return void 0;
1088
1088
  const headers = record2(raw.http_headers ?? raw.headers);
@@ -1131,8 +1131,8 @@ function normalizeCodexMcp(name2, value, source, base, environment = process.env
1131
1131
  if (environment[variable] !== void 0 && env[variable] === void 0)
1132
1132
  env[variable] = environment[variable];
1133
1133
  }
1134
- const cwd = typeof raw.cwd === "string" ? resolve(base, raw.cwd) : base;
1135
- const command = raw.command.includes("/") && !isAbsolute(raw.command) ? resolve(cwd, raw.command) : raw.command;
1134
+ const cwd = typeof raw.cwd === "string" ? resolve(base, raw.cwd) : defaultCwd;
1135
+ const command = raw.command.includes("/") && !isAbsolute(raw.command) ? resolve(typeof raw.cwd === "string" ? cwd : base, raw.command) : raw.command;
1136
1136
  return {
1137
1137
  ...common,
1138
1138
  transport: "stdio",
@@ -1151,9 +1151,29 @@ async function directories(path) {
1151
1151
  }
1152
1152
  }
1153
1153
  async function manifestAt(root) {
1154
+ const text = await optionalText(join2(root, "plugin.json"));
1155
+ if (text !== void 0) {
1156
+ const manifest = object(JSON.parse(text));
1157
+ if (manifest.$schema === "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json") {
1158
+ const inline = manifest.extensions?.["com.openai"];
1159
+ const overlay = inline !== null && typeof inline === "object" && !Array.isArray(inline) ? inline : object(
1160
+ JSON.parse(
1161
+ await optionalText(
1162
+ join2(root, ".codex-plugin", "plugin.json")
1163
+ ) ?? "{}"
1164
+ )
1165
+ );
1166
+ return {
1167
+ root,
1168
+ manifest: { ...manifest, ...overlay, name: manifest.name },
1169
+ portable: true
1170
+ };
1171
+ }
1172
+ }
1154
1173
  for (const name2 of [".codex-plugin", ".claude-plugin"]) {
1155
- const text = await optionalText(join2(root, name2, "plugin.json"));
1156
- if (text !== void 0) return { root, manifest: object(JSON.parse(text)) };
1174
+ const text2 = await optionalText(join2(root, name2, "plugin.json"));
1175
+ if (text2 !== void 0)
1176
+ return { root, manifest: object(JSON.parse(text2)), portable: false };
1157
1177
  }
1158
1178
  return void 0;
1159
1179
  }
@@ -1182,7 +1202,7 @@ async function resolvePlugin(home, id, config) {
1182
1202
  }
1183
1203
  function expandPlugin(value, root, home) {
1184
1204
  if (typeof value === "string")
1185
- return value.replaceAll("${CLAUDE_PLUGIN_ROOT}", root).replaceAll("${CODEX_PLUGIN_ROOT}", root).replaceAll("${CODEX_HOME}", home);
1205
+ return value.replaceAll("${PLUGIN_ROOT}", root).replaceAll("${CLAUDE_PLUGIN_ROOT}", root).replaceAll("${CODEX_PLUGIN_ROOT}", root).replaceAll("${CODEX_HOME}", home);
1186
1206
  if (Array.isArray(value)) return value.map((v) => expandPlugin(v, root, home));
1187
1207
  if (value && typeof value === "object")
1188
1208
  return Object.fromEntries(
@@ -1190,6 +1210,22 @@ function expandPlugin(value, root, home) {
1190
1210
  );
1191
1211
  return value;
1192
1212
  }
1213
+ function restrictMcpFallback(entry, policy) {
1214
+ if (policy.enabled === false) return void 0;
1215
+ const requested = Array.isArray(policy.enabled_tools) ? strings(policy.enabled_tools) : void 0;
1216
+ const enabledTools = requested && entry.enabledTools ? entry.enabledTools.filter((name2) => requested.includes(name2)) : requested ?? entry.enabledTools;
1217
+ return {
1218
+ ...entry,
1219
+ ...enabledTools ? { enabledTools } : {},
1220
+ disabledTools: [
1221
+ .../* @__PURE__ */ new Set([
1222
+ ...entry.disabledTools,
1223
+ ...strings(policy.disabled_tools),
1224
+ ...Object.entries(object(policy.tools)).filter(([, tool]) => object(tool).enabled === false).map(([name2]) => name2)
1225
+ ])
1226
+ ]
1227
+ };
1228
+ }
1193
1229
  async function discoverCodexSnapshot(home, options, context = {}) {
1194
1230
  const config = await readCodexConfig(
1195
1231
  home,
@@ -1207,24 +1243,38 @@ async function discoverCodexSnapshot(home, options, context = {}) {
1207
1243
  items: []
1208
1244
  };
1209
1245
  const names = /* @__PURE__ */ new Set();
1210
- const addServers = (servers, source, base, plugin = "") => {
1211
- for (const [name2, raw] of Object.entries(servers)) {
1246
+ const addServers = (servers, source, base, plugin = "", policies = {}) => {
1247
+ for (const [name2, declaration] of Object.entries(servers)) {
1248
+ const policy = Object.fromEntries(
1249
+ Object.entries(object(policies[name2])).filter(
1250
+ ([key]) => ["enabled", "enabled_tools", "disabled_tools", "tools"].includes(key)
1251
+ )
1252
+ );
1253
+ const raw = mergeConfig(object(declaration), policy);
1212
1254
  const publicName = plugin ? `${plugin}-${name2}` : name2;
1213
1255
  if (object(raw).enabled === false || options.mcpAllowlist.length && !options.mcpAllowlist.includes(name2)) {
1214
1256
  result.items.push({ name: publicName, kind: "mcp", state: "disabled" });
1215
1257
  continue;
1216
1258
  }
1217
1259
  try {
1218
- const value = normalizeCodexMcp(publicName, raw, source, base);
1260
+ const value = normalizeCodexMcp(
1261
+ publicName,
1262
+ raw,
1263
+ source,
1264
+ base,
1265
+ process.env,
1266
+ context.cwd ?? base
1267
+ );
1219
1268
  if (!value) continue;
1220
1269
  if (names.has(value.serverName)) throw new Error("namespace-conflict");
1221
1270
  names.add(value.serverName);
1222
- result.mcp.push(value);
1271
+ result.mcp.push({ ...value, originalName: name2 });
1223
1272
  } catch (error) {
1224
1273
  const previous = context.previous?.mcp.find(
1225
1274
  (entry) => entry.serverName === serverName(publicName)
1226
1275
  );
1227
- if (previous) result.mcp.push(previous);
1276
+ const retained = previous && restrictMcpFallback(previous, raw);
1277
+ if (retained) result.mcp.push(retained);
1228
1278
  const reason = [
1229
1279
  "missing-environment",
1230
1280
  "unsupported-transport",
@@ -1258,9 +1308,9 @@ async function discoverCodexSnapshot(home, options, context = {}) {
1258
1308
  });
1259
1309
  continue;
1260
1310
  }
1261
- const { root, manifest } = plugin;
1311
+ const { root, manifest, portable } = plugin;
1262
1312
  const name2 = String(manifest.name ?? id.split("@")[0]);
1263
- const declared = manifest.skills ?? "./skills";
1313
+ const declared = portable ? "./skills" : manifest.skills ?? "./skills";
1264
1314
  const skillRoots = [];
1265
1315
  for (const entry of Array.isArray(declared) ? declared : [declared]) {
1266
1316
  if (typeof entry === "string")
@@ -1271,7 +1321,10 @@ async function discoverCodexSnapshot(home, options, context = {}) {
1271
1321
  });
1272
1322
  }
1273
1323
  let servers = manifest.mcpServers;
1274
- if (typeof servers === "string") {
1324
+ if (portable) {
1325
+ const text = await optionalText(join2(root, "mcp.json"));
1326
+ servers = text === void 0 ? void 0 : JSON.parse(text);
1327
+ } else if (typeof servers === "string") {
1275
1328
  const text = await optionalText(resolve(root, servers));
1276
1329
  if (text === void 0) throw new Error("missing-plugin-mcp-file");
1277
1330
  servers = JSON.parse(text);
@@ -1285,7 +1338,13 @@ async function discoverCodexSnapshot(home, options, context = {}) {
1285
1338
  home
1286
1339
  );
1287
1340
  result.skillRoots.push(...skillRoots);
1288
- addServers(object(expanded), `plugin:${id}`, root, name2);
1341
+ addServers(
1342
+ object(expanded),
1343
+ `plugin:${id}`,
1344
+ root,
1345
+ name2,
1346
+ object(object(setting).mcp_servers)
1347
+ );
1289
1348
  result.items.push({ name: id, kind: "plugin", state: "loaded" });
1290
1349
  for (const field of ["hooks", "apps", "commands"]) {
1291
1350
  if (manifest[field])
@@ -1297,11 +1356,16 @@ async function discoverCodexSnapshot(home, options, context = {}) {
1297
1356
  });
1298
1357
  }
1299
1358
  } catch {
1300
- result.mcp.push(
1301
- ...context.previous?.mcp.filter(
1302
- (entry) => entry.source === `plugin:${id}`
1303
- ) ?? []
1304
- );
1359
+ const policies = object(object(setting).mcp_servers);
1360
+ for (const entry of context.previous?.mcp ?? []) {
1361
+ if (entry.source !== `plugin:${id}`) continue;
1362
+ if (!entry.originalName && Object.keys(policies).length) continue;
1363
+ const retained = restrictMcpFallback(
1364
+ entry,
1365
+ object(policies[entry.originalName ?? ""])
1366
+ );
1367
+ if (retained) result.mcp.push(retained);
1368
+ }
1305
1369
  result.skillRoots.push(
1306
1370
  ...context.previous?.skillRoots.filter(
1307
1371
  (entry) => entry.pluginId === id
@@ -1548,6 +1612,49 @@ function installCodexInstructions(ctx, home) {
1548
1612
  );
1549
1613
  }
1550
1614
 
1615
+ // src/codex-mcp-metadata.ts
1616
+ import { AsyncLocalStorage } from "node:async_hooks";
1617
+ import { Client } from "@modelcontextprotocol/client";
1618
+ var scope = new AsyncLocalStorage();
1619
+ var users = 0;
1620
+ var restore;
1621
+ function installCodexMcpMetadata() {
1622
+ if (users++ === 0) {
1623
+ const original = Client.prototype.callTool;
1624
+ const callTool = function(params, options) {
1625
+ const call = scope.getStore();
1626
+ const metadata = call?.active() ? call.metadata : void 0;
1627
+ return original.call(
1628
+ this,
1629
+ metadata ? {
1630
+ ...params,
1631
+ _meta: { ...params._meta, "x-codex-turn-metadata": metadata }
1632
+ } : params,
1633
+ options
1634
+ );
1635
+ };
1636
+ Client.prototype.callTool = callTool;
1637
+ restore = () => {
1638
+ if (Client.prototype.callTool === callTool)
1639
+ Client.prototype.callTool = original;
1640
+ };
1641
+ }
1642
+ let active = true;
1643
+ return {
1644
+ run(metadata, operation) {
1645
+ return scope.run({ active: () => active, metadata }, operation);
1646
+ },
1647
+ dispose() {
1648
+ if (!active) return;
1649
+ active = false;
1650
+ if (--users === 0) {
1651
+ restore?.();
1652
+ restore = void 0;
1653
+ }
1654
+ }
1655
+ };
1656
+ }
1657
+
1551
1658
  // src/codex.ts
1552
1659
  var fingerprint = (v) => createHash5("sha256").update(JSON.stringify(v)).digest("hex");
1553
1660
  function publicToolName(server, name2) {
@@ -1629,6 +1736,7 @@ function installCodexCompatibility(ctx, config) {
1629
1736
  dispose: async () => {
1630
1737
  }
1631
1738
  };
1739
+ const metadata = config.importMcpServers ? installCodexMcpMetadata() : void 0;
1632
1740
  const scopedSnapshot = async (cwd, signal, fallback) => {
1633
1741
  const key = resolve3(cwd);
1634
1742
  const previous = scopedSnapshots.get(key) ?? fallback ?? latest;
@@ -1841,7 +1949,22 @@ function installCodexCompatibility(ctx, config) {
1841
1949
  if (enabledTools && !enabledTools.includes(definition.name) || disabledTools.includes(definition.name))
1842
1950
  return () => {
1843
1951
  };
1844
- return target.register(definition);
1952
+ return target.register({
1953
+ ...definition,
1954
+ execute(args, execution) {
1955
+ const agent = execution.agent ?? owner.agent;
1956
+ const boundary = agent.session ? child.agents.turnBoundaryOf?.(agent.session) : void 0;
1957
+ const turn = boundary?.openTurnStartSeq;
1958
+ const turnMetadata = typeof agent.id === "string" && agent.id && turn != null ? {
1959
+ session_id: agent.id,
1960
+ turn_id: `${agent.id}:${turn}`
1961
+ } : void 0;
1962
+ return metadata.run(
1963
+ turnMetadata,
1964
+ () => definition.execute(args, execution)
1965
+ );
1966
+ }
1967
+ });
1845
1968
  };
1846
1969
  const value = Reflect.get(target, key);
1847
1970
  return typeof value === "function" ? value.bind(target) : value;
@@ -1984,9 +2107,15 @@ function installCodexCompatibility(ctx, config) {
1984
2107
  agents.delete(agent);
1985
2108
  await owner.tail.catch(() => {
1986
2109
  });
1987
- await Promise.all(
1988
- [...owner.clients.values()].map((client) => client.fiber.dispose())
2110
+ const results = await Promise.allSettled(
2111
+ [...owner.clients.values()].map(async (client) => client.fiber.dispose())
1989
2112
  );
2113
+ const errors = results.flatMap(
2114
+ (result) => result.status === "rejected" ? [result.reason] : []
2115
+ );
2116
+ if (errors.length === 1) throw errors[0];
2117
+ if (errors.length > 1)
2118
+ throw new AggregateError(errors, "Codex MCP client cleanup failed");
1990
2119
  };
1991
2120
  const reconcile = async () => {
1992
2121
  if (closed) return;
@@ -2082,11 +2211,25 @@ function installCodexCompatibility(ctx, config) {
2082
2211
  const dispose = () => stopping ??= (async () => {
2083
2212
  closed = true;
2084
2213
  if (timer) clearInterval(timer);
2085
- await Promise.all(injections.map((fiber) => fiber.dispose()));
2086
- await tail;
2087
- await Promise.all([...agents.keys()].map(disposeAgent));
2088
- skillDispose?.();
2089
- await Promise.all(skills.map((s) => s.dispose()));
2214
+ const errors = [];
2215
+ const settle = async (actions) => {
2216
+ const results = await Promise.allSettled(
2217
+ actions.map(async (action) => action())
2218
+ );
2219
+ for (const result of results)
2220
+ if (result.status === "rejected") errors.push(result.reason);
2221
+ };
2222
+ await settle(injections.map((fiber) => () => fiber.dispose()));
2223
+ await settle([() => tail]);
2224
+ await settle([...agents.keys()].map((agent) => () => disposeAgent(agent)));
2225
+ await settle([
2226
+ () => metadata?.dispose(),
2227
+ () => skillDispose?.(),
2228
+ ...skills.map((provider) => () => provider.dispose())
2229
+ ]);
2230
+ if (errors.length === 1) throw errors[0];
2231
+ if (errors.length > 1)
2232
+ throw new AggregateError(errors, "Codex compatibility cleanup failed");
2090
2233
  })();
2091
2234
  ctx.effect(() => dispose, "codex-compatibility");
2092
2235
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-gpt-adapter",
3
- "version": "0.2.2",
3
+ "version": "0.2.3",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -122,7 +122,8 @@
122
122
  "react-dom": "18.3.1",
123
123
  "typescript": "^5.8.2",
124
124
  "vitest": "^3.0.8",
125
- "yaml": "^2.8.1"
125
+ "yaml": "^2.8.1",
126
+ "@modelcontextprotocol/client": "2.2.0"
126
127
  },
127
128
  "peerDependencies": {
128
129
  "@deepseek-ai/cordis": "4.1.0",
@@ -141,7 +142,8 @@
141
142
  "@deepseek-ai/dsh-typert-protocol": "0.2.2-rc.1",
142
143
  "@deepseek-ai/dsh-util-values": "0.2.2-rc.1",
143
144
  "@deepseek-ai/schemastery": "3.19.0",
144
- "@earendil-works/pi-ai": "1.3.0"
145
+ "@earendil-works/pi-ai": "1.3.0",
146
+ "@modelcontextprotocol/client": "2.2.0"
145
147
  },
146
148
  "dependencies": {
147
149
  "smol-toml": "1.9.1"