@nyssance/codex-acp-v2 0.3.5 → 0.5.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.0 — 2026-09-11
4
+
5
+ ### Added
6
+
7
+ - `providers/set` hints `_meta.codex.bearerToken` (written as `experimental_bearer_token`)
8
+ and `_meta.codex.config` (top-level Codex thread-config overrides that ride with the
9
+ gateway; a `model_catalog_json` file is read so gateway models carry their real reasoning
10
+ levels and modalities). `providers/disable` removes both from live threads.
11
+ - Catalog mode (`_meta.codex.mode: "catalog"`): the gateway's models are offered next to
12
+ Codex's in every session's `model` option as their own select group; selecting one moves
13
+ only that session to the gateway (unsubscribe + resume, materializing an empty thread
14
+ first), selecting a Codex model moves it back. `session/new` / `session/fork` honour
15
+ `_meta.alwith.model`; `session/resume` follows the provider Codex recorded for the thread.
16
+ Advertised as `capabilities._meta.codex.providerCatalog`.
17
+
18
+ ### Fixed
19
+
20
+ - A thread another Codex client is writing ("already has an active writer", a file lock
21
+ across the Codex home) opens as a read-only session through `thread/read`, like Codex's
22
+ TUI: history replays, the response carries `_meta.codex.readOnly`, prompts and config
23
+ changes are refused with `-32600`, and a later resume retries the real thing.
24
+
25
+ ## 0.4.0 — 2026-09-10
26
+
27
+ ### Added
28
+
29
+ - Skills and plugins extension surface: `_codex/skills_list`, `_codex/skills_config_write`,
30
+ `_codex/plugin_list`, `_codex/plugin_installed`, `_codex/plugin_install`,
31
+ `_codex/plugin_uninstall`, `_codex/plugin_read`, `_codex/marketplace_add`,
32
+ `_codex/marketplace_remove`, `_codex/marketplace_upgrade` pass the Codex v2 shapes
33
+ through verbatim; advertised as `capabilities._meta.codex.skills` / `.plugins`.
34
+ - `_codex/skills_changed` notification: forwarded from Codex `skills/changed` and sent
35
+ after every catalog mutation made through the adapter.
36
+
3
37
  ## 0.3.5 — 2026-09-07
4
38
 
5
39
  ### Fixed
package/README.md CHANGED
@@ -21,6 +21,12 @@ compatibility layer: everything is written against `@agentclientprotocol/sdk/exp
21
21
  hiding is the `_codex/session_archive` / `_codex/session_unarchive` extension pair
22
22
  (declared as `capabilities._meta.codex.archive`), and `session/list` pages the archive
23
23
  when called with `_meta: {codex: {archived: true}}`.
24
+ - **Skills and plugins**: `_codex/skills_list`, `_codex/skills_config_write`,
25
+ `_codex/plugin_list` / `_codex/plugin_installed` / `_codex/plugin_install` /
26
+ `_codex/plugin_uninstall` / `_codex/plugin_read` and `_codex/marketplace_add` /
27
+ `_codex/marketplace_remove` / `_codex/marketplace_upgrade` pass Codex's own request
28
+ and response shapes through (declared as `capabilities._meta.codex.skills` / `.plugins`);
29
+ `_codex/skills_changed` tells the client to refetch either catalog.
24
30
  - **Asynchronous prompts**: `session/prompt` returns immediately; the turn is
25
31
  reported through `state_update` frames (`running`, `requires_action` while a
26
32
  permission or form is open, `idle` with `stopReason` and token `usage`).
package/dist/index.js CHANGED
@@ -3406,8 +3406,8 @@ var require_main = __commonJS(function(exports) {
3406
3406
  exports.createMessageConnection = createMessageConnection;
3407
3407
  var ril_1 = __importDefault(require_ril());
3408
3408
  ril_1.default.install();
3409
- var path7 = __importStar(__require("path"));
3410
- var os2 = __importStar(__require("os"));
3409
+ var path8 = __importStar(__require("path"));
3410
+ var os3 = __importStar(__require("os"));
3411
3411
  var fs7 = __importStar(__require("fs"));
3412
3412
  var crypto_1 = __require("crypto");
3413
3413
  var net_1 = __require("net");
@@ -3555,7 +3555,7 @@ var require_main = __commonJS(function(exports) {
3555
3555
  }
3556
3556
  let randomLength = 32;
3557
3557
  const fixedLength = "/lsp-.sock".length;
3558
- const tmpDir = fs7.realpathSync(XDG_RUNTIME_DIR ?? os2.tmpdir());
3558
+ const tmpDir = fs7.realpathSync(XDG_RUNTIME_DIR ?? os3.tmpdir());
3559
3559
  const limit = safeIpcPathLengths.get(process.platform);
3560
3560
  if (limit !== undefined) {
3561
3561
  randomLength = Math.min(limit - tmpDir.length - fixedLength, randomLength);
@@ -3564,7 +3564,7 @@ var require_main = __commonJS(function(exports) {
3564
3564
  throw new Error(`Unable to generate a random pipe name with ${randomLength} characters.`);
3565
3565
  }
3566
3566
  const randomSuffix = (0, crypto_1.randomBytes)(Math.floor(randomLength / 2)).toString("hex");
3567
- return path7.join(tmpDir, `lsp-${randomSuffix}.sock`);
3567
+ return path8.join(tmpDir, `lsp-${randomSuffix}.sock`);
3568
3568
  }
3569
3569
  function createClientPipeTransport(pipeName, encoding = "utf-8") {
3570
3570
  let connectResolve;
@@ -21721,7 +21721,7 @@ var package_default = {
21721
21721
  publishConfig: {
21722
21722
  access: "public"
21723
21723
  },
21724
- version: "0.3.5",
21724
+ version: "0.5.0",
21725
21725
  description: "Native ACP v2 agent for the OpenAI Codex app-server",
21726
21726
  packageManager: "bun@1.4.0",
21727
21727
  engines: {
@@ -21838,7 +21838,7 @@ function classifyTurnError(info) {
21838
21838
  }
21839
21839
 
21840
21840
  // src/agent/CodexAgent.ts
21841
- import path6 from "path";
21841
+ import path7 from "path";
21842
21842
 
21843
21843
  // src/util/logger.ts
21844
21844
  import fs from "fs";
@@ -23982,7 +23982,7 @@ function modelLacksReasoning(model) {
23982
23982
  const options = supportedEfforts(model);
23983
23983
  return options.length > 0 && options.every((option) => option.reasoningEffort === "none");
23984
23984
  }
23985
- function modelConfigOption(catalog, current) {
23985
+ function modelConfigOption(catalog, current, gatewayGroup = null) {
23986
23986
  const visible = catalog.filter((model) => !model.hidden || model.id === current);
23987
23987
  const options = visible.map((model) => ({
23988
23988
  value: model.id,
@@ -23992,14 +23992,25 @@ function modelConfigOption(catalog, current) {
23992
23992
  if (!options.some((option) => option.value === current)) {
23993
23993
  options.unshift({ value: current, name: current, description: null });
23994
23994
  }
23995
- return {
23995
+ const base = {
23996
23996
  configId: MODEL_CONFIG_ID,
23997
23997
  name: "Model",
23998
23998
  description: "Model Codex uses for this session",
23999
23999
  category: "model",
24000
24000
  type: "select",
24001
- currentValue: current,
24002
- options
24001
+ currentValue: current
24002
+ };
24003
+ if (gatewayGroup === null)
24004
+ return { ...base, options };
24005
+ const gatewayIds = new Set(gatewayGroup.modelIds);
24006
+ const native = options.filter((option) => !gatewayIds.has(option.value));
24007
+ const gateway = options.filter((option) => gatewayIds.has(option.value));
24008
+ return {
24009
+ ...base,
24010
+ options: [
24011
+ { groupId: "codex", name: "Codex", options: native },
24012
+ { groupId: "custom-gateway", name: gatewayGroup.name, options: gateway }
24013
+ ]
24003
24014
  };
24004
24015
  }
24005
24016
  function effortConfigOption(efforts, current) {
@@ -25955,7 +25966,7 @@ function sessionConfigOptions(session) {
25955
25966
  const model = findModel(session.catalog, session.model.model);
25956
25967
  const options = [
25957
25968
  modeConfigOption(session.mode),
25958
- modelConfigOption(session.catalog, session.model.model),
25969
+ modelConfigOption(session.catalog, session.model.model, session.gatewayGroup),
25959
25970
  effortConfigOption(supportedEfforts(model), session.model.effort),
25960
25971
  collaborationModeConfigOption(session.collaborationMode)
25961
25972
  ];
@@ -26057,10 +26068,12 @@ function firstLine(text2) {
26057
26068
  }
26058
26069
 
26059
26070
  // src/agent/providers.ts
26071
+ import { readFileSync } from "fs";
26072
+ import os2 from "os";
26073
+ import path6 from "path";
26060
26074
  var OPENAI_PROVIDER_ID = "openai";
26061
26075
  var GATEWAY_MODEL_PROVIDER = "custom-gateway";
26062
26076
  var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
26063
-
26064
26077
  class ProviderRouting {
26065
26078
  baseConfig;
26066
26079
  configuredProvider;
@@ -26078,12 +26091,14 @@ class ProviderRouting {
26078
26091
  return this.gateway;
26079
26092
  }
26080
26093
  list() {
26094
+ const routed = this.gateway !== null && this.gateway.mode === "route";
26081
26095
  return {
26082
26096
  providers: [{
26083
26097
  providerId: OPENAI_PROVIDER_ID,
26084
26098
  supported: ["openai"],
26085
26099
  required: false,
26086
- current: this.gateway ? { apiType: "openai", baseUrl: this.gateway.baseUrl } : { apiType: "openai", baseUrl: this.nativeBaseUrl() }
26100
+ current: routed && this.gateway ? { apiType: "openai", baseUrl: this.gateway.baseUrl } : { apiType: "openai", baseUrl: this.nativeBaseUrl() },
26101
+ ...this.gateway && !routed ? { _meta: { codex: { mode: "catalog", gateway: { name: this.gateway.name, baseUrl: this.gateway.baseUrl, models: this.gateway.models.map((model) => model.id) } } } } : {}
26087
26102
  }]
26088
26103
  };
26089
26104
  }
@@ -26104,37 +26119,83 @@ class ProviderRouting {
26104
26119
  headers: { ...request.headers ?? {} },
26105
26120
  name: hints.name ?? "Client-configured gateway",
26106
26121
  models: hints.models,
26107
- model: hints.model
26122
+ model: hints.model,
26123
+ bearerToken: hints.bearerToken,
26124
+ config: hints.config,
26125
+ mode: hints.mode,
26126
+ catalogEntries: hints.catalogEntries
26108
26127
  };
26109
26128
  }
26129
+ get mode() {
26130
+ return this.gateway?.mode ?? "route";
26131
+ }
26132
+ isGatewayModel(modelId) {
26133
+ return this.gateway !== null && this.gateway.models.some((model) => model.id === modelId);
26134
+ }
26135
+ gatewayGroup() {
26136
+ if (!this.gateway || this.gateway.mode !== "catalog" || this.gateway.models.length === 0)
26137
+ return null;
26138
+ return { name: this.gateway.name, modelIds: this.gateway.models.map((model) => model.id) };
26139
+ }
26110
26140
  disable(request) {
26111
26141
  if (request.providerId === OPENAI_PROVIDER_ID)
26112
26142
  this.gateway = null;
26113
26143
  }
26114
26144
  modelProvider() {
26115
- return this.gateway ? GATEWAY_MODEL_PROVIDER : this.configuredProvider;
26145
+ return this.gateway && this.gateway.mode === "route" ? GATEWAY_MODEL_PROVIDER : this.configuredProvider;
26116
26146
  }
26117
- threadConfig() {
26118
- if (!this.gateway)
26147
+ routesByDefault() {
26148
+ return this.gateway !== null && this.gateway.mode === "route";
26149
+ }
26150
+ threadConfig(onGateway = this.routesByDefault()) {
26151
+ if (!this.gateway || !onGateway)
26119
26152
  return this.baseConfig;
26120
- const existing = isRecord2(this.baseConfig["model_providers"]) ? this.baseConfig["model_providers"] : {};
26153
+ return this.applyGateway(this.baseConfig, this.gateway);
26154
+ }
26155
+ routeConfig(config2, onGateway) {
26156
+ return this.rebind(config2, this, onGateway);
26157
+ }
26158
+ overrideKeys() {
26159
+ return this.gateway ? Object.keys(this.gateway.config) : [];
26160
+ }
26161
+ rebind(config2, previous, onGateway = this.routesByDefault()) {
26162
+ const next = { ...config2 };
26163
+ for (const key of previous.overrideKeys()) {
26164
+ if (key in this.baseConfig)
26165
+ next[key] = this.baseConfig[key];
26166
+ else
26167
+ delete next[key];
26168
+ }
26169
+ if (!this.gateway || !onGateway) {
26170
+ delete next["model_providers"];
26171
+ if (isRecord2(this.baseConfig["model_providers"]))
26172
+ next["model_providers"] = this.baseConfig["model_providers"];
26173
+ return next;
26174
+ }
26175
+ return this.applyGateway(next, this.gateway);
26176
+ }
26177
+ applyGateway(config2, gateway) {
26178
+ const existing = isRecord2(config2["model_providers"]) ? config2["model_providers"] : {};
26179
+ const entry = {
26180
+ name: gateway.name,
26181
+ base_url: gateway.baseUrl,
26182
+ wire_api: "responses",
26183
+ http_headers: { "X-Client-Feature-ID": "codex", ...gateway.headers }
26184
+ };
26185
+ if (gateway.bearerToken !== null)
26186
+ entry["experimental_bearer_token"] = gateway.bearerToken;
26121
26187
  return {
26122
- ...this.baseConfig,
26123
- model_providers: {
26124
- ...existing,
26125
- [GATEWAY_MODEL_PROVIDER]: {
26126
- name: this.gateway.name,
26127
- base_url: this.gateway.baseUrl,
26128
- wire_api: "responses",
26129
- http_headers: { "X-Client-Feature-ID": "codex", ...this.gateway.headers }
26130
- }
26131
- }
26188
+ ...config2,
26189
+ ...gateway.config,
26190
+ model_providers: { ...existing, [GATEWAY_MODEL_PROVIDER]: entry }
26132
26191
  };
26133
26192
  }
26134
26193
  catalog(codexCatalog) {
26135
26194
  if (!this.gateway || this.gateway.models.length === 0)
26136
26195
  return codexCatalog;
26137
- return this.gateway.models.map((model, index) => gatewayModel(model, index === 0, codexCatalog[0]));
26196
+ const gateway = this.gateway;
26197
+ const own = gateway.models.map((model, index) => gatewayModel(model, gateway.mode === "route" && index === 0, codexCatalog[0], gateway.catalogEntries.get(model.id)));
26198
+ return gateway.mode === "catalog" ? [...codexCatalog, ...own] : own;
26138
26199
  }
26139
26200
  nativeBaseUrl() {
26140
26201
  const providers = this.baseConfig["model_providers"];
@@ -26165,22 +26226,80 @@ function readHints(meta) {
26165
26226
  }
26166
26227
  const model = typeof alwith["model"] === "string" && alwith["model"].length > 0 ? alwith["model"] : null;
26167
26228
  const name = typeof codex["name"] === "string" && codex["name"].length > 0 ? codex["name"] : null;
26168
- return { models, model, name };
26229
+ const bearerToken = typeof codex["bearerToken"] === "string" && codex["bearerToken"].length > 0 ? codex["bearerToken"] : null;
26230
+ const rawConfig = codex["config"];
26231
+ if (rawConfig !== undefined && !isRecord2(rawConfig)) {
26232
+ throw RequestError.invalidParams({ config: rawConfig }, "_meta.codex.config must be an object of top-level Codex config keys");
26233
+ }
26234
+ const config2 = {};
26235
+ if (isRecord2(rawConfig)) {
26236
+ for (const [key, value] of Object.entries(rawConfig)) {
26237
+ if (key === "model_providers")
26238
+ throw RequestError.invalidParams({ config: rawConfig }, "_meta.codex.config cannot set model_providers; that entry is derived from baseUrl and headers");
26239
+ config2[key] = value;
26240
+ }
26241
+ }
26242
+ const rawMode = codex["mode"];
26243
+ if (rawMode !== undefined && rawMode !== "route" && rawMode !== "catalog") {
26244
+ throw RequestError.invalidParams({ mode: rawMode }, '_meta.codex.mode must be "route" or "catalog"');
26245
+ }
26246
+ const mode = rawMode === "catalog" ? "catalog" : "route";
26247
+ if (mode === "catalog" && models.length === 0) {
26248
+ throw RequestError.invalidParams(undefined, `_meta.codex.mode "catalog" needs _meta.alwith.models: the models offered next to Codex's`);
26249
+ }
26250
+ const catalogPath = config2["model_catalog_json"];
26251
+ const catalogEntries = typeof catalogPath === "string" ? readCatalogFile(catalogPath) : new Map;
26252
+ return { models, model, name, bearerToken, config: config2, mode, catalogEntries };
26253
+ }
26254
+ function readCatalogFile(catalogPath) {
26255
+ const resolved = catalogPath.startsWith("~/") ? path6.join(os2.homedir(), catalogPath.slice(2)) : catalogPath;
26256
+ let parsed;
26257
+ try {
26258
+ parsed = JSON.parse(readFileSync(resolved, "utf8"));
26259
+ } catch (error40) {
26260
+ throw RequestError.invalidParams({ model_catalog_json: catalogPath }, `model_catalog_json could not be read as JSON: ${error40 instanceof Error ? error40.message : String(error40)}`);
26261
+ }
26262
+ const models = isRecord2(parsed) && Array.isArray(parsed["models"]) ? parsed["models"] : null;
26263
+ if (models === null)
26264
+ throw RequestError.invalidParams({ model_catalog_json: catalogPath }, "model_catalog_json must be {models: [...]} as Codex expects");
26265
+ const entries = new Map;
26266
+ for (const entry of models) {
26267
+ if (!isRecord2(entry) || typeof entry["slug"] !== "string")
26268
+ continue;
26269
+ const levels = Array.isArray(entry["supported_reasoning_levels"]) ? entry["supported_reasoning_levels"] : [];
26270
+ const efforts = [];
26271
+ for (const level of levels) {
26272
+ if (!isRecord2(level) || typeof level["effort"] !== "string")
26273
+ continue;
26274
+ efforts.push({ reasoningEffort: level["effort"], description: typeof level["description"] === "string" ? level["description"] : "" });
26275
+ }
26276
+ const modalities = Array.isArray(entry["input_modalities"]) ? entry["input_modalities"].filter((value) => value === "text" || value === "image") : null;
26277
+ entries.set(entry["slug"], {
26278
+ displayName: typeof entry["display_name"] === "string" ? entry["display_name"] : null,
26279
+ description: typeof entry["description"] === "string" ? entry["description"] : null,
26280
+ efforts,
26281
+ defaultEffort: typeof entry["default_reasoning_level"] === "string" ? entry["default_reasoning_level"] : null,
26282
+ inputModalities: modalities
26283
+ });
26284
+ }
26285
+ return entries;
26169
26286
  }
26170
- function gatewayModel(model, isDefault, template) {
26287
+ function gatewayModel(model, isDefault, template, entry) {
26288
+ const efforts = entry && entry.efforts.length > 0 ? entry.efforts : template?.supportedReasoningEfforts ?? [];
26289
+ const defaultEffort = entry?.defaultEffort ?? template?.defaultReasoningEffort ?? "medium";
26171
26290
  return {
26172
26291
  id: model.id,
26173
26292
  model: model.id,
26174
26293
  upgrade: null,
26175
26294
  upgradeInfo: null,
26176
26295
  availabilityNux: null,
26177
- displayName: model.name,
26178
- description: model.description ?? "",
26296
+ displayName: model.name !== model.id ? model.name : entry?.displayName ?? model.name,
26297
+ description: model.description ?? entry?.description ?? "",
26179
26298
  modelSpecialty: null,
26180
26299
  hidden: false,
26181
- supportedReasoningEfforts: template?.supportedReasoningEfforts ?? [],
26182
- defaultReasoningEffort: template?.defaultReasoningEffort ?? "medium",
26183
- inputModalities: ["text", "image"],
26300
+ supportedReasoningEfforts: efforts,
26301
+ defaultReasoningEffort: defaultEffort,
26302
+ inputModalities: entry?.inputModalities ?? ["text", "image"],
26184
26303
  supportsPersonality: false,
26185
26304
  multiAgentVersion: null,
26186
26305
  additionalSpeedTiers: [],
@@ -26283,6 +26402,7 @@ class CodexAgent {
26283
26402
  if (!this.changingSkillRoots)
26284
26403
  for (const runtime of this.sessions.values())
26285
26404
  this.refreshAvailableCommands(runtime);
26405
+ this.notifySkillsChanged();
26286
26406
  }
26287
26407
  if (notification.method === "account/updated")
26288
26408
  this.refreshAccounts().catch((error40) => logger.error("refreshing account failed", error40));
@@ -26322,7 +26442,7 @@ class CodexAgent {
26322
26442
  additionalDirectories: {}
26323
26443
  },
26324
26444
  providers: {},
26325
- _meta: { codex: { archive: true, seedHistory: true } }
26445
+ _meta: { codex: { archive: true, seedHistory: true, skills: true, plugins: true, providerCatalog: true } }
26326
26446
  },
26327
26447
  authMethods: authMethods(this.capabilities, this.env)
26328
26448
  };
@@ -26407,23 +26527,27 @@ class CodexAgent {
26407
26527
  try {
26408
26528
  change(candidate);
26409
26529
  const catalog = await this.withCodex(() => this.codex.allModels(true));
26410
- const modelProvider = await this.resolveModelProvider(candidate);
26411
- for (const runtime of this.sessions.values()) {
26530
+ const nativeProvider = await this.resolveModelProvider(candidate);
26531
+ const wholesale = this.providers.active !== null && this.providers.mode === "route" || candidate.active !== null && candidate.mode === "route";
26532
+ const moving = [...this.sessions.values()].filter((runtime) => wholesale || runtime.modelProvider === GATEWAY_MODEL_PROVIDER);
26533
+ const targets = new Map(moving.map((runtime) => [runtime, candidate.active !== null && (candidate.mode === "route" || runtime.modelProvider === GATEWAY_MODEL_PROVIDER)]));
26534
+ for (const runtime of moving) {
26412
26535
  try {
26413
26536
  await this.codex.threadTurnsList({ threadId: runtime.session.id, limit: 1, itemsView: "notLoaded" });
26414
26537
  } catch (error40) {
26415
- throw RequestError.invalidRequest({ sessionId: runtime.session.id, details: errorMessage(error40) }, "Cannot reload this session's history for a provider change; close empty sessions and configure the provider before creating them, or retry after history is available");
26538
+ if (wholesale) {
26539
+ throw RequestError.invalidRequest({ sessionId: runtime.session.id, details: errorMessage(error40) }, "Cannot reload this session's history for a provider change; close empty sessions and configure the provider before creating them, or retry after history is available");
26540
+ }
26541
+ await this.withCodex(() => this.codex.threadInjectItems({ threadId: runtime.session.id, items: [providerChangeNote()] }));
26416
26542
  }
26417
26543
  }
26418
- for (const runtime of this.sessions.values()) {
26419
- const config2 = { ...runtime.config };
26420
- const providers = candidate.threadConfig()["model_providers"];
26421
- if (providers === undefined)
26422
- delete config2["model_providers"];
26423
- else
26424
- config2["model_providers"] = providers;
26425
- const nextCatalog = candidate.catalog(catalog);
26426
- const requestedModel = candidate.active?.model ?? (findModel(nextCatalog, runtime.session.model.model) ? runtime.session.model.model : null);
26544
+ const nextCatalog = candidate.catalog(catalog);
26545
+ for (const runtime of moving) {
26546
+ const onGateway = targets.get(runtime) === true;
26547
+ const config2 = candidate.rebind(runtime.config, this.providers, onGateway);
26548
+ const modelProvider = onGateway ? GATEWAY_MODEL_PROVIDER : nativeProvider;
26549
+ const keep = findModel(nextCatalog, runtime.session.model.model) && candidate.isGatewayModel(runtime.session.model.model) === onGateway;
26550
+ const requestedModel = candidate.mode === "route" ? candidate.active?.model ?? (keep ? runtime.session.model.model : null) : keep ? runtime.session.model.model : null;
26427
26551
  const selection = resolveModelSelection(nextCatalog, requestedModel, requestedModel === runtime.session.model.model ? runtime.session.model.effort : null);
26428
26552
  attempted.push(runtime);
26429
26553
  await this.codex.threadUnsubscribe({ threadId: runtime.session.id });
@@ -26435,15 +26559,21 @@ class CodexAgent {
26435
26559
  model: selection.model,
26436
26560
  excludeTurns: true
26437
26561
  }));
26438
- staged.set(runtime, { config: config2, catalog: nextCatalog, model: resolveModelSelection(nextCatalog, candidate.active?.model ?? thread.model, thread.reasoningEffort) });
26562
+ staged.set(runtime, { config: config2, catalog: nextCatalog, model: resolveModelSelection(nextCatalog, requestedModel ?? thread.model, thread.reasoningEffort) });
26439
26563
  }
26440
26564
  this.providers = candidate;
26441
- for (const [runtime, next] of staged) {
26442
- runtime.config = next.config;
26443
- runtime.modelProvider = modelProvider;
26444
- runtime.session.catalog = next.catalog;
26445
- runtime.session.model = next.model;
26446
- runtime.stale = false;
26565
+ for (const runtime of this.sessions.values()) {
26566
+ const next = staged.get(runtime);
26567
+ if (next) {
26568
+ runtime.config = next.config;
26569
+ runtime.modelProvider = targets.get(runtime) === true ? GATEWAY_MODEL_PROVIDER : nativeProvider;
26570
+ runtime.session.model = next.model;
26571
+ runtime.stale = false;
26572
+ }
26573
+ runtime.session.catalog = nextCatalog;
26574
+ runtime.session.gatewayGroup = candidate.gatewayGroup();
26575
+ if (!next)
26576
+ runtime.session.model = resolveModelSelection(nextCatalog, findModel(nextCatalog, runtime.session.model.model) ? runtime.session.model.model : null, runtime.session.model.effort);
26447
26577
  runtime.client.update({ sessionUpdate: "config_option_update", configOptions: sessionConfigOptions(runtime.session), _meta: { codex: { routing: { stale: false } } } }).catch((error40) => logger.error("Provider config notification failed", error40));
26448
26578
  }
26449
26579
  } catch (error40) {
@@ -26482,7 +26612,8 @@ class CodexAgent {
26482
26612
  }
26483
26613
  async resumeSession(params, signal) {
26484
26614
  const runtime = await this.openSession({ kind: "resume", request: params }, signal);
26485
- return { configOptions: sessionConfigOptions(runtime.session) };
26615
+ const configOptions = sessionConfigOptions(runtime.session);
26616
+ return runtime.readOnly ? { configOptions, _meta: { codex: { readOnly: true, reason: "active_writer" } } } : { configOptions };
26486
26617
  }
26487
26618
  async forkSession(params, signal) {
26488
26619
  const runtime = await this.openSession({ kind: "fork", request: params }, signal);
@@ -26518,7 +26649,7 @@ class CodexAgent {
26518
26649
  if (open2.kind !== "new")
26519
26650
  this.assertUnsubscribeSettled(open2.request.sessionId);
26520
26651
  const { request } = open2;
26521
- if (typeof request.cwd !== "string" || !path6.isAbsolute(request.cwd)) {
26652
+ if (typeof request.cwd !== "string" || !path7.isAbsolute(request.cwd)) {
26522
26653
  throw RequestError.invalidParams({ cwd: request.cwd }, "cwd must be an absolute path");
26523
26654
  }
26524
26655
  if (open2.kind === "resume" && open2.request.replayFrom != null && open2.request.replayFrom.type !== "start") {
@@ -26531,25 +26662,44 @@ class CodexAgent {
26531
26662
  this.assertUnsubscribeSettled(open2.request.sessionId);
26532
26663
  }
26533
26664
  const mcpServers = request.mcpServers ?? [];
26665
+ const requestedModel = requestedModelOf(request._meta);
26666
+ let onGateway = this.providers.routesByDefault();
26667
+ if (this.providers.active !== null && this.providers.mode === "catalog") {
26668
+ if (requestedModel !== null)
26669
+ onGateway = this.providers.isGatewayModel(requestedModel);
26670
+ else if (open2.kind === "resume") {
26671
+ const known = await this.withCodex(() => this.codex.threadRead({ threadId: open2.request.sessionId, includeTurns: false }));
26672
+ onGateway = known.thread.modelProvider === GATEWAY_MODEL_PROVIDER;
26673
+ }
26674
+ }
26534
26675
  let accountVersion = this.accountGeneration;
26535
26676
  const account = await this.withCodex(() => this.codex.accountRead({ refreshToken: false }));
26536
- if (this.providers.active === null && account.requiresOpenaiAuth && account.account === null) {
26677
+ if (!onGateway && account.requiresOpenaiAuth && account.account === null) {
26537
26678
  throw RequestError.authRequired(undefined, "Log in to Codex first (auth/login)");
26538
26679
  }
26539
26680
  const existingMcp = mcpServers.length > 0 ? await this.withCodex(() => this.configuredMcpServerNames(request.cwd)) : new Set;
26540
- const config2 = buildThreadConfig(this.providers.threadConfig(), request.cwd, additionalDirectories, mcpServers, existingMcp);
26681
+ const config2 = buildThreadConfig(this.providers.threadConfig(onGateway), request.cwd, additionalDirectories, mcpServers, existingMcp);
26541
26682
  const mcpStartupGeneration = this.codex.mcpStartupGeneration;
26542
- const modelProvider = await this.withCodex(() => this.resolveModelProvider());
26683
+ const modelProvider = onGateway ? GATEWAY_MODEL_PROVIDER : await this.withCodex(() => this.resolveModelProvider());
26684
+ const startModel = onGateway ? requestedModel ?? this.providers.active?.model ?? null : requestedModel;
26685
+ let readOnly = false;
26543
26686
  const { thread, skills } = await this.withSkillsContext(request.cwd, additionalDirectories, async (skills2) => ({ skills: skills2, thread: await this.withCodex(async () => {
26544
26687
  if (signal?.aborted)
26545
26688
  throw RequestError.requestCancelled(undefined, "Session opening was cancelled");
26546
26689
  switch (open2.kind) {
26547
26690
  case "new":
26548
- return await this.codex.threadStart({ config: config2, cwd: request.cwd, modelProvider });
26691
+ return await this.codex.threadStart({ config: config2, cwd: request.cwd, modelProvider, model: startModel });
26549
26692
  case "resume":
26550
- return await this.codex.threadResume({ threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, excludeTurns: true });
26693
+ try {
26694
+ return await this.codex.threadResume({ threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, model: startModel, excludeTurns: true });
26695
+ } catch (error40) {
26696
+ if (!errorMessage(error40).includes("already has an active writer"))
26697
+ throw error40;
26698
+ readOnly = true;
26699
+ return readOnlyThread((await this.codex.threadRead({ threadId: open2.request.sessionId, includeTurns: false })).thread, request.cwd);
26700
+ }
26551
26701
  case "fork":
26552
- return await this.codex.threadFork({ threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, excludeTurns: true });
26702
+ return await this.codex.threadFork({ threadId: open2.request.sessionId, config: config2, cwd: request.cwd, modelProvider, model: startModel, excludeTurns: true });
26553
26703
  }
26554
26704
  }) }));
26555
26705
  const sessionId = thread.thread.id;
@@ -26577,15 +26727,15 @@ class CodexAgent {
26577
26727
  accountVersion = this.accountGeneration;
26578
26728
  openedAccount = (await this.withCodex(() => this.codex.accountRead({ refreshToken: false }))).account;
26579
26729
  }
26580
- const gateway = this.providers.active;
26581
26730
  const catalog = this.providers.catalog(codexCatalog);
26582
- const model = resolveModelSelection(catalog, gateway?.model ?? thread.model, thread.reasoningEffort);
26731
+ const model = resolveModelSelection(catalog, startModel ?? thread.model, thread.reasoningEffort);
26583
26732
  const session = {
26584
26733
  id: sessionId,
26585
26734
  cwd: request.cwd,
26586
26735
  additionalDirectories,
26587
26736
  mcpServerNames: mcpServerNames(mcpServers).filter((name) => !existingMcp.has(name)),
26588
26737
  catalog,
26738
+ gatewayGroup: this.providers.gatewayGroup(),
26589
26739
  model,
26590
26740
  mode: initialAgentMode(this.env),
26591
26741
  collaborationMode: DEFAULT_COLLABORATION_MODE,
@@ -26598,7 +26748,7 @@ class CodexAgent {
26598
26748
  contextWindow: null,
26599
26749
  closed: false
26600
26750
  };
26601
- const runtime = this.installRuntime(session, capabilities, config2, thread.modelProvider);
26751
+ const runtime = this.installRuntime(session, capabilities, config2, onGateway ? GATEWAY_MODEL_PROVIDER : thread.modelProvider, readOnly);
26602
26752
  const cancelOpening = () => {
26603
26753
  runtime.session.closed = true;
26604
26754
  runtime.lifetime.abort();
@@ -26645,7 +26795,7 @@ class CodexAgent {
26645
26795
  stopObservingOpen();
26646
26796
  }
26647
26797
  }
26648
- installRuntime(session, capabilities, config2, modelProvider) {
26798
+ installRuntime(session, capabilities, config2, modelProvider, readOnly = false) {
26649
26799
  const client = new ClientSession(session.id, this.link, capabilities);
26650
26800
  const bridge = new EventBridge(client, session);
26651
26801
  const turnContext = new TurnContext(session.id);
@@ -26655,7 +26805,7 @@ class CodexAgent {
26655
26805
  };
26656
26806
  const approval = new CodexApprovalHandler(client, turnContext, signal);
26657
26807
  const elicitation = new CodexElicitationHandler(client, turnContext, signal);
26658
- const runtime = { config: config2, modelProvider, stale: false, session, client, bridge, turnContext, elicitation, lifetime: new AbortController, queue: Promise.resolve() };
26808
+ const runtime = { config: config2, modelProvider, stale: false, readOnly, session, client, bridge, turnContext, elicitation, lifetime: new AbortController, queue: Promise.resolve() };
26659
26809
  const drained = (operation) => async (params) => {
26660
26810
  await abortable(this.drain(runtime), this.codex.disconnectSignal);
26661
26811
  return await operation(params);
@@ -26841,6 +26991,61 @@ class CodexAgent {
26841
26991
  await this.withCodex(() => this.codex.threadUnarchive({ threadId: params.sessionId }));
26842
26992
  return {};
26843
26993
  }
26994
+ async skillsList(params) {
26995
+ this.requireInitialized("_codex/skills_list");
26996
+ return await this.withCodex(() => this.codex.skillsList(params));
26997
+ }
26998
+ async skillsConfigWrite(params) {
26999
+ this.requireInitialized("_codex/skills_config_write");
27000
+ const response = await this.withCodex(() => this.codex.skillsConfigWrite(params));
27001
+ this.notifySkillsChanged();
27002
+ return response;
27003
+ }
27004
+ async pluginList(params) {
27005
+ this.requireInitialized("_codex/plugin_list");
27006
+ return await this.withCodex(() => this.codex.pluginList(params));
27007
+ }
27008
+ async pluginInstalled(params) {
27009
+ this.requireInitialized("_codex/plugin_installed");
27010
+ return await this.withCodex(() => this.codex.pluginInstalled(params));
27011
+ }
27012
+ async pluginInstall(params) {
27013
+ this.requireInitialized("_codex/plugin_install");
27014
+ const response = await this.withCodex(() => this.codex.pluginInstall(params));
27015
+ this.notifySkillsChanged();
27016
+ return response;
27017
+ }
27018
+ async pluginUninstall(params) {
27019
+ this.requireInitialized("_codex/plugin_uninstall");
27020
+ await this.withCodex(() => this.codex.pluginUninstall(params));
27021
+ this.notifySkillsChanged();
27022
+ return {};
27023
+ }
27024
+ async pluginRead(params) {
27025
+ this.requireInitialized("_codex/plugin_read");
27026
+ return await this.withCodex(() => this.codex.pluginRead(params));
27027
+ }
27028
+ async marketplaceAdd(params) {
27029
+ this.requireInitialized("_codex/marketplace_add");
27030
+ const response = await this.withCodex(() => this.codex.marketplaceAdd(params));
27031
+ this.notifySkillsChanged();
27032
+ return response;
27033
+ }
27034
+ async marketplaceRemove(params) {
27035
+ this.requireInitialized("_codex/marketplace_remove");
27036
+ await this.withCodex(() => this.codex.marketplaceRemove(params));
27037
+ this.notifySkillsChanged();
27038
+ return {};
27039
+ }
27040
+ async marketplaceUpgrade(params) {
27041
+ this.requireInitialized("_codex/marketplace_upgrade");
27042
+ const response = await this.withCodex(() => this.codex.marketplaceUpgrade(params));
27043
+ this.notifySkillsChanged();
27044
+ return response;
27045
+ }
27046
+ notifySkillsChanged() {
27047
+ this.link.notify("_codex/skills_changed", {}).catch((error40) => logger.error("skills change notification failed", error40));
27048
+ }
26844
27049
  async closeRuntime(sessionId) {
26845
27050
  const deadline = performance.now() + this.closeGraceMs;
26846
27051
  const runtime = this.sessions.get(sessionId);
@@ -26867,7 +27072,7 @@ class CodexAgent {
26867
27072
  runtime.session.activeTurn = null;
26868
27073
  turn.resolveFinished();
26869
27074
  }
26870
- const unsubscribe = this.unsubscribeThread(sessionId);
27075
+ const unsubscribe = runtime.readOnly ? Promise.resolve() : this.unsubscribeThread(sessionId);
26871
27076
  let remoteUnsubscribe = "confirmed";
26872
27077
  try {
26873
27078
  if (!await within(unsubscribe, Math.max(0, deadline - performance.now()))) {
@@ -26896,11 +27101,46 @@ class CodexAgent {
26896
27101
  }
26897
27102
  async setSessionConfigOptionInternal(params) {
26898
27103
  const runtime = this.runtime(params.sessionId, "session/set_config_option");
27104
+ assertWritable(runtime);
27105
+ if (params.configId === MODEL_CONFIG_ID && this.providers.mode === "catalog" && typeof params.value === "string") {
27106
+ const toGateway = this.providers.isGatewayModel(params.value);
27107
+ if (toGateway !== (runtime.modelProvider === GATEWAY_MODEL_PROVIDER))
27108
+ await this.rerouteSession(runtime, toGateway, params.value);
27109
+ }
26899
27110
  await this.withCodex(() => applyConfigOption(runtime.session, this.codex, params));
26900
27111
  const configOptions = sessionConfigOptions(runtime.session);
26901
27112
  await runtime.client.update({ sessionUpdate: "config_option_update", configOptions });
26902
27113
  return { configOptions };
26903
27114
  }
27115
+ async rerouteSession(runtime, toGateway, modelId) {
27116
+ const { session } = runtime;
27117
+ this.assertRoutingAvailable();
27118
+ if (session.activeTurn !== null) {
27119
+ throw RequestError.invalidRequest({ sessionId: session.id }, "The model provider cannot change while a turn is running; cancel it or wait for idle");
27120
+ }
27121
+ try {
27122
+ await this.codex.threadTurnsList({ threadId: session.id, limit: 1, itemsView: "notLoaded" });
27123
+ } catch {
27124
+ await this.withCodex(() => this.codex.threadInjectItems({ threadId: session.id, items: [providerChangeNote()] }));
27125
+ }
27126
+ const config2 = this.providers.routeConfig(runtime.config, toGateway);
27127
+ const modelProvider = toGateway ? GATEWAY_MODEL_PROVIDER : await this.withCodex(() => this.resolveModelProvider());
27128
+ await this.codex.threadUnsubscribe({ threadId: session.id });
27129
+ try {
27130
+ await this.withCodex(() => this.codex.threadResume({ threadId: session.id, cwd: session.cwd, config: config2, modelProvider, model: modelId, excludeTurns: true }));
27131
+ } catch (error40) {
27132
+ try {
27133
+ await this.codex.threadResume({ threadId: session.id, cwd: session.cwd, config: runtime.config, modelProvider: runtime.modelProvider, model: session.model.model, excludeTurns: true });
27134
+ } catch (rollbackError) {
27135
+ runtime.stale = true;
27136
+ logger.error("Provider rollback failed", rollbackError, { sessionId: session.id });
27137
+ }
27138
+ throw RequestError.internalError({ sessionId: session.id, stale: runtime.stale }, `Could not move the session to ${toGateway ? "the gateway" : "Codex's provider"}: ${errorMessage(error40)}`);
27139
+ }
27140
+ runtime.config = config2;
27141
+ runtime.modelProvider = modelProvider;
27142
+ runtime.stale = false;
27143
+ }
26904
27144
  runtime(sessionId, method) {
26905
27145
  this.requireInitialized(method);
26906
27146
  const runtime = this.sessions.get(sessionId);
@@ -26909,16 +27149,20 @@ class CodexAgent {
26909
27149
  return runtime;
26910
27150
  }
26911
27151
  async prompt(params) {
27152
+ return await this.promptInternal(params);
27153
+ }
27154
+ async promptInternal(params, allowReentry = false) {
26912
27155
  this.assertRoutingAvailable();
26913
- if (this.sessionMutations.has(params.sessionId))
27156
+ if (!allowReentry && this.sessionMutations.has(params.sessionId))
26914
27157
  throw RequestError.invalidRequest({ sessionId: params.sessionId }, "Session lifecycle or configuration work is in progress; retry when it finishes");
26915
27158
  const runtime = this.runtime(params.sessionId, "session/prompt");
27159
+ assertWritable(runtime);
26916
27160
  if (runtime.stale)
26917
27161
  throw RequestError.invalidRequest({ sessionId: params.sessionId, _meta: { codex: { routing: { stale: true } } } }, "Session routing is stale; resume the session or successfully change providers before prompting");
26918
- const { session } = runtime;
26919
27162
  if (!Array.isArray(params.prompt) || params.prompt.length === 0) {
26920
27163
  throw RequestError.invalidParams(undefined, "prompt must contain at least one content block");
26921
27164
  }
27165
+ const { session } = runtime;
26922
27166
  const model = findModel(session.catalog, session.model.model);
26923
27167
  if (!modelSupportsImages(model) && params.prompt.some((block) => block.type === "image")) {
26924
27168
  throw RequestError.invalidParams({ model: session.model.model }, "The current model does not support image input");
@@ -26943,7 +27187,7 @@ class CodexAgent {
26943
27187
  } catch (error40) {
26944
27188
  if (this.completedTurns.get(turn) === turnId || runtime.session.activeTurn !== turn) {
26945
27189
  await turn.finished;
26946
- return await this.prompt(params);
27190
+ return await this.promptInternal(params, true);
26947
27191
  }
26948
27192
  throw RequestError.invalidRequest({ sessionId: params.sessionId, turnId }, `Could not steer the running turn: ${errorMessage(error40)}`);
26949
27193
  }
@@ -27211,7 +27455,7 @@ ${error40.additionalDetails}` : error40.message;
27211
27455
  }
27212
27456
  withSkillsContext(cwd, additionalDirectories, operation) {
27213
27457
  const run = async () => {
27214
- const roots = additionalDirectories.map((root) => path6.join(root, ".agents", "skills"));
27458
+ const roots = additionalDirectories.map((root) => path7.join(root, ".agents", "skills"));
27215
27459
  if (roots.length !== this.skillRoots.length || roots.some((root, index) => root !== this.skillRoots[index])) {
27216
27460
  this.changingSkillRoots = true;
27217
27461
  try {
@@ -27245,7 +27489,7 @@ ${error40.additionalDetails}` : error40.message;
27245
27489
  }
27246
27490
  async refreshAvailableCommands(runtime) {
27247
27491
  const run = async () => {
27248
- const roots = runtime.session.additionalDirectories.map((root) => path6.join(root, ".agents", "skills"));
27492
+ const roots = runtime.session.additionalDirectories.map((root) => path7.join(root, ".agents", "skills"));
27249
27493
  if (runtime.session.closed || roots.length !== this.skillRoots.length || roots.some((root, i) => root !== this.skillRoots[i]))
27250
27494
  return;
27251
27495
  return await this.loadSkills(runtime.session.cwd, runtime.session.additionalDirectories);
@@ -27348,6 +27592,16 @@ function interruptedTurn(threadId) {
27348
27592
  turn: { id: "", items: [], itemsView: "notLoaded", status: "interrupted", error: null, startedAt: null, completedAt: null, durationMs: null }
27349
27593
  };
27350
27594
  }
27595
+ function objectParams() {
27596
+ return (raw) => {
27597
+ if (raw === undefined || raw === null)
27598
+ return {};
27599
+ if (typeof raw !== "object" || Array.isArray(raw)) {
27600
+ throw RequestError.invalidParams({ params: raw }, "expected a params object");
27601
+ }
27602
+ return raw;
27603
+ };
27604
+ }
27351
27605
  function parseSessionIdParams(raw) {
27352
27606
  const sessionId = raw?.sessionId;
27353
27607
  if (typeof sessionId !== "string" || sessionId.length === 0) {
@@ -27359,6 +27613,34 @@ function archivedFilter(meta) {
27359
27613
  const codex = meta?.codex;
27360
27614
  return codex?.archived === true;
27361
27615
  }
27616
+ function providerChangeNote() {
27617
+ return { type: "message", role: "developer", content: [{ type: "input_text", text: "The user changed this conversation's model before sending a message." }] };
27618
+ }
27619
+ function assertWritable(runtime) {
27620
+ if (!runtime.readOnly)
27621
+ return;
27622
+ throw RequestError.invalidRequest({ codex: { readOnly: true } }, "Thread is open in another Codex client");
27623
+ }
27624
+ function readOnlyThread(thread, cwd) {
27625
+ return {
27626
+ thread,
27627
+ model: thread.model ?? "",
27628
+ modelProvider: thread.modelProvider,
27629
+ serviceTier: null,
27630
+ cwd,
27631
+ instructionSources: [],
27632
+ approvalPolicy: "on-request",
27633
+ approvalsReviewer: "user",
27634
+ sandbox: { type: "readOnly", networkAccess: false },
27635
+ reasoningEffort: thread.reasoningEffort,
27636
+ turnsBackwardsCursor: null,
27637
+ itemsBackwardsCursor: null
27638
+ };
27639
+ }
27640
+ function requestedModelOf(meta) {
27641
+ const raw = meta?.alwith?.model;
27642
+ return typeof raw === "string" && raw.length > 0 ? raw : null;
27643
+ }
27362
27644
  function seedHistoryOf(meta) {
27363
27645
  const raw = meta?.codex?.seedHistory;
27364
27646
  if (raw === undefined || raw === null)
@@ -27405,7 +27687,7 @@ function createAgentApp(options) {
27405
27687
  };
27406
27688
  return agent({ name: options.info.name }).onConnect((connection) => {
27407
27689
  agent2 = new CodexAgent(connection.client, options);
27408
- }).onRequest(methods.agent.initialize, (ctx) => current().initialize(ctx.params)).onRequest(methods.agent.auth.login, (ctx) => current().login(ctx.params, ctx.requestId)).onRequest(methods.agent.auth.logout, (ctx) => current().logout(ctx.params)).onRequest(methods.agent.providers.list, (ctx) => current().listProviders(ctx.params)).onRequest(methods.agent.providers.set, (ctx) => current().setProvider(ctx.params)).onRequest(methods.agent.providers.disable, (ctx) => current().disableProvider(ctx.params)).onRequest(methods.agent.session.new, (ctx) => current().newSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.resume, (ctx) => current().resumeSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.fork, (ctx) => current().forkSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.list, (ctx) => current().listSessions(ctx.params)).onRequest(methods.agent.session.close, (ctx) => current().closeSession(ctx.params)).onRequest(methods.agent.session.delete, (ctx) => current().deleteSession(ctx.params)).onRequest("_codex/session_archive", { parse: parseSessionIdParams }, (ctx) => current().archiveSession(ctx.params)).onRequest("_codex/session_unarchive", { parse: parseSessionIdParams }, (ctx) => current().unarchiveSession(ctx.params)).onRequest(methods.agent.session.setConfigOption, (ctx) => current().setSessionConfigOption(ctx.params)).onRequest(methods.agent.session.prompt, (ctx) => current().prompt(ctx.params)).onNotification(methods.agent.session.cancel, (ctx) => current().cancel(ctx.params));
27690
+ }).onRequest(methods.agent.initialize, (ctx) => current().initialize(ctx.params)).onRequest(methods.agent.auth.login, (ctx) => current().login(ctx.params, ctx.requestId)).onRequest(methods.agent.auth.logout, (ctx) => current().logout(ctx.params)).onRequest(methods.agent.providers.list, (ctx) => current().listProviders(ctx.params)).onRequest(methods.agent.providers.set, (ctx) => current().setProvider(ctx.params)).onRequest(methods.agent.providers.disable, (ctx) => current().disableProvider(ctx.params)).onRequest(methods.agent.session.new, (ctx) => current().newSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.resume, (ctx) => current().resumeSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.fork, (ctx) => current().forkSession(ctx.params, ctx.signal)).onRequest(methods.agent.session.list, (ctx) => current().listSessions(ctx.params)).onRequest(methods.agent.session.close, (ctx) => current().closeSession(ctx.params)).onRequest(methods.agent.session.delete, (ctx) => current().deleteSession(ctx.params)).onRequest("_codex/session_archive", { parse: parseSessionIdParams }, (ctx) => current().archiveSession(ctx.params)).onRequest("_codex/session_unarchive", { parse: parseSessionIdParams }, (ctx) => current().unarchiveSession(ctx.params)).onRequest("_codex/skills_list", { parse: objectParams() }, (ctx) => current().skillsList(ctx.params)).onRequest("_codex/skills_config_write", { parse: objectParams() }, (ctx) => current().skillsConfigWrite(ctx.params)).onRequest("_codex/plugin_list", { parse: objectParams() }, (ctx) => current().pluginList(ctx.params)).onRequest("_codex/plugin_installed", { parse: objectParams() }, (ctx) => current().pluginInstalled(ctx.params)).onRequest("_codex/plugin_install", { parse: objectParams() }, (ctx) => current().pluginInstall(ctx.params)).onRequest("_codex/plugin_uninstall", { parse: objectParams() }, (ctx) => current().pluginUninstall(ctx.params)).onRequest("_codex/plugin_read", { parse: objectParams() }, (ctx) => current().pluginRead(ctx.params)).onRequest("_codex/marketplace_add", { parse: objectParams() }, (ctx) => current().marketplaceAdd(ctx.params)).onRequest("_codex/marketplace_remove", { parse: objectParams() }, (ctx) => current().marketplaceRemove(ctx.params)).onRequest("_codex/marketplace_upgrade", { parse: objectParams() }, (ctx) => current().marketplaceUpgrade(ctx.params)).onRequest(methods.agent.session.setConfigOption, (ctx) => current().setSessionConfigOption(ctx.params)).onRequest(methods.agent.session.prompt, (ctx) => current().prompt(ctx.params)).onNotification(methods.agent.session.cancel, (ctx) => current().cancel(ctx.params));
27409
27691
  }
27410
27692
 
27411
27693
  // src/codex/AppServerClient.ts
@@ -27788,6 +28070,33 @@ class AppServerClient {
27788
28070
  skillsExtraRootsSet(params) {
27789
28071
  return this.send({ method: "skills/extraRoots/set", params });
27790
28072
  }
28073
+ skillsConfigWrite(params) {
28074
+ return this.send({ method: "skills/config/write", params });
28075
+ }
28076
+ pluginList(params) {
28077
+ return this.send({ method: "plugin/list", params });
28078
+ }
28079
+ pluginInstalled(params) {
28080
+ return this.send({ method: "plugin/installed", params });
28081
+ }
28082
+ pluginInstall(params) {
28083
+ return this.send({ method: "plugin/install", params });
28084
+ }
28085
+ pluginUninstall(params) {
28086
+ return this.send({ method: "plugin/uninstall", params });
28087
+ }
28088
+ pluginRead(params) {
28089
+ return this.send({ method: "plugin/read", params });
28090
+ }
28091
+ marketplaceAdd(params) {
28092
+ return this.send({ method: "marketplace/add", params });
28093
+ }
28094
+ marketplaceRemove(params) {
28095
+ return this.send({ method: "marketplace/remove", params });
28096
+ }
28097
+ marketplaceUpgrade(params) {
28098
+ return this.send({ method: "marketplace/upgrade", params });
28099
+ }
27791
28100
  accountRead(params) {
27792
28101
  return this.send({ method: "account/read", params });
27793
28102
  }
@@ -27826,7 +28135,7 @@ function threadIdOf(notification) {
27826
28135
 
27827
28136
  // src/codex/process.ts
27828
28137
  import { existsSync } from "fs";
27829
- import path7 from "path";
28138
+ import path8 from "path";
27830
28139
  import { spawn, spawnSync } from "child_process";
27831
28140
  import { createRequire } from "module";
27832
28141
 
@@ -28119,11 +28428,11 @@ function resolveCodexLauncher(codexPath) {
28119
28428
  const target = targets[platform2];
28120
28429
  if (!target)
28121
28430
  throw new Error(`Unsupported Codex platform: ${platform2}`);
28122
- let vendor = path7.resolve(path7.dirname(launcher), "..", "vendor");
28431
+ let vendor = path8.resolve(path8.dirname(launcher), "..", "vendor");
28123
28432
  try {
28124
- vendor = path7.join(path7.dirname(platformRequire.resolve(`@openai/codex-${platform2}/package.json`)), "vendor");
28433
+ vendor = path8.join(path8.dirname(platformRequire.resolve(`@openai/codex-${platform2}/package.json`)), "vendor");
28125
28434
  } catch {}
28126
- const command = path7.join(vendor, target, "bin", process.platform === "win32" ? "codex.exe" : "codex");
28435
+ const command = path8.join(vendor, target, "bin", process.platform === "win32" ? "codex.exe" : "codex");
28127
28436
  if (!existsSync(command))
28128
28437
  throw new Error(`Missing Codex platform binary: ${command}`);
28129
28438
  return { command, prefixArgs: [], shell: false };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.3.5",
6
+ "version": "0.5.0",
7
7
  "description": "Native ACP v2 agent for the OpenAI Codex app-server",
8
8
  "packageManager": "bun@1.4.0",
9
9
  "engines": {