@khorsheed/dsh-local-agent-codex 0.2.1 → 0.2.2

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,4 +1,9 @@
1
1
  # 变更记录
2
+ ## 0.2.2(2026-10-27)
3
+
4
+ - **成员按会话的沙箱模式运行**:会话设成只读或可写,codex 成员跟着用同样的权限。
5
+ - 成员的工具调用保留结构化参数和 MCP 结果;命令跑完一定带上输出和退出码。
6
+
2
7
  ## 0.2.1(2026-10-27)
3
8
 
4
9
  - **修复几个模式套件一起装时设置存不上**:卡片上的开关和设置改存在本 harness 自己的目录里,不再写进配置文件;原来配好的值照常生效。
package/lib/client.js CHANGED
@@ -343,30 +343,30 @@ window.__ModuleLoader__.load({
343
343
  document.head.appendChild(tag);
344
344
  }
345
345
  var ProviderAuthBlock_module_css_default = {
346
- "promptText": "On5p7W_promptText",
347
- "status": "On5p7W_status",
348
- "title": "On5p7W_title",
349
- "rowCard": "On5p7W_rowCard",
350
- "loginButton": "On5p7W_loginButton",
351
- "retryButton": "On5p7W_retryButton",
352
- "rowActions": "On5p7W_rowActions",
353
- "logoutButton": "On5p7W_logoutButton",
346
+ "dotOff": "On5p7W_dotOff",
347
+ "rows": "On5p7W_rows",
348
+ "rosterError": "On5p7W_rosterError",
354
349
  "section": "On5p7W_section",
355
- "intro": "On5p7W_intro",
356
- "dotPending": "On5p7W_dotPending",
357
- "dotOn": "On5p7W_dotOn",
358
350
  "rowName": "On5p7W_rowName",
351
+ "status": "On5p7W_status",
352
+ "loginButton": "On5p7W_loginButton",
353
+ "loginPrompt": "On5p7W_loginPrompt",
354
+ "promptText": "On5p7W_promptText",
359
355
  "codeInput": "On5p7W_codeInput",
356
+ "rowHead": "On5p7W_rowHead",
357
+ "rowTag": "On5p7W_rowTag",
360
358
  "openPage": "On5p7W_openPage",
361
- "rowPending": "On5p7W_rowPending",
359
+ "rowCard": "On5p7W_rowCard",
360
+ "logoutButton": "On5p7W_logoutButton",
361
+ "dotOn": "On5p7W_dotOn",
362
+ "intro": "On5p7W_intro",
362
363
  "rowIdentity": "On5p7W_rowIdentity",
363
- "rows": "On5p7W_rows",
364
- "rosterError": "On5p7W_rosterError",
365
- "rowTag": "On5p7W_rowTag",
366
- "dotOff": "On5p7W_dotOff",
364
+ "dotPending": "On5p7W_dotPending",
365
+ "rowActions": "On5p7W_rowActions",
366
+ "title": "On5p7W_title",
367
367
  "credentialDot": "On5p7W_credentialDot",
368
- "rowHead": "On5p7W_rowHead",
369
- "loginPrompt": "On5p7W_loginPrompt"
368
+ "rowPending": "On5p7W_rowPending",
369
+ "retryButton": "On5p7W_retryButton"
370
370
  };
371
371
  //#endregion
372
372
  //#region ../local-agent/src/client/ProviderAuthBlock.tsx
@@ -645,10 +645,10 @@ window.__ModuleLoader__.load({
645
645
  document.head.appendChild(tag);
646
646
  }
647
647
  var AuthStatusDot_module_css_default = {
648
- "dotOn": "OFjNUG_dotOn",
649
648
  "credentialDot": "OFjNUG_credentialDot",
650
649
  "dotOff": "OFjNUG_dotOff",
651
- "dotPending": "OFjNUG_dotPending"
650
+ "dotPending": "OFjNUG_dotPending",
651
+ "dotOn": "OFjNUG_dotOn"
652
652
  };
653
653
  //#endregion
654
654
  //#region ../local-agent/src/client/AuthStatusDot.tsx
@@ -674,39 +674,39 @@ window.__ModuleLoader__.load({
674
674
  document.head.appendChild(tag);
675
675
  }
676
676
  var ModelListSection_module_css_default = {
677
- "dialogItemLabel": "_4eeJ7W_dialogItemLabel",
678
- "dialogSelectAll": "_4eeJ7W_dialogSelectAll",
679
- "row": "_4eeJ7W_row",
680
- "block": "_4eeJ7W_block",
677
+ "modelRowValue": "_4eeJ7W_modelRowValue",
681
678
  "blockTitle": "_4eeJ7W_blockTitle",
679
+ "dialogItemValue": "_4eeJ7W_dialogItemValue",
680
+ "dialogBackdrop": "_4eeJ7W_dialogBackdrop",
681
+ "block": "_4eeJ7W_block",
682
+ "dialogTitle": "_4eeJ7W_dialogTitle",
683
+ "row": "_4eeJ7W_row",
682
684
  "modelInput": "_4eeJ7W_modelInput",
683
- "dialogItem": "_4eeJ7W_dialogItem",
684
- "hint": "_4eeJ7W_hint",
685
- "dialog": "_4eeJ7W_dialog",
686
- "modelList": "_4eeJ7W_modelList",
687
685
  "modelRowDragging": "_4eeJ7W_modelRowDragging",
688
- "dialogList": "_4eeJ7W_dialogList",
686
+ "dialog": "_4eeJ7W_dialog",
687
+ "dialogActions": "_4eeJ7W_dialogActions",
689
688
  "modelAdd": "_4eeJ7W_modelAdd",
690
- "errorText": "_4eeJ7W_errorText",
691
- "dialogTitle": "_4eeJ7W_dialogTitle",
689
+ "modelDrag": "_4eeJ7W_modelDrag",
690
+ "dialogSelectAll": "_4eeJ7W_dialogSelectAll",
691
+ "dialogItemLabel": "_4eeJ7W_dialogItemLabel",
692
+ "dialogToolbar": "_4eeJ7W_dialogToolbar",
693
+ "dialogList": "_4eeJ7W_dialogList",
692
694
  "modelSave": "_4eeJ7W_modelSave",
693
- "modelRowValue": "_4eeJ7W_modelRowValue",
695
+ "dialogHead": "_4eeJ7W_dialogHead",
696
+ "dialogHint": "_4eeJ7W_dialogHint",
697
+ "modelList": "_4eeJ7W_modelList",
698
+ "dialogItem": "_4eeJ7W_dialogItem",
699
+ "modelDefaultChip": "_4eeJ7W_modelDefaultChip",
700
+ "errorText": "_4eeJ7W_errorText",
694
701
  "saved": "_4eeJ7W_saved",
695
- "modelRow": "_4eeJ7W_modelRow",
696
- "dialogActions": "_4eeJ7W_dialogActions",
697
- "dialogClose": "_4eeJ7W_dialogClose",
698
- "modelDrag": "_4eeJ7W_modelDrag",
702
+ "hint": "_4eeJ7W_hint",
703
+ "dialogConfirm": "_4eeJ7W_dialogConfirm",
704
+ "modelRowLabel": "_4eeJ7W_modelRowLabel",
699
705
  "info": "_4eeJ7W_info",
700
706
  "modelDelete": "_4eeJ7W_modelDelete",
701
707
  "dialogItemAdded": "_4eeJ7W_dialogItemAdded",
702
- "dialogItemValue": "_4eeJ7W_dialogItemValue",
703
- "dialogConfirm": "_4eeJ7W_dialogConfirm",
704
- "dialogBackdrop": "_4eeJ7W_dialogBackdrop",
705
- "dialogHead": "_4eeJ7W_dialogHead",
706
- "dialogToolbar": "_4eeJ7W_dialogToolbar",
707
- "dialogHint": "_4eeJ7W_dialogHint",
708
- "modelDefaultChip": "_4eeJ7W_modelDefaultChip",
709
- "modelRowLabel": "_4eeJ7W_modelRowLabel"
708
+ "modelRow": "_4eeJ7W_modelRow",
709
+ "dialogClose": "_4eeJ7W_dialogClose"
710
710
  };
711
711
  //#endregion
712
712
  //#region ../local-agent/src/client/ModelListSection.tsx
@@ -1079,32 +1079,32 @@ window.__ModuleLoader__.load({
1079
1079
  }
1080
1080
  var SettingsCard_module_css_default = {
1081
1081
  "configuredChip": "oWITGW_configuredChip",
1082
- "switchOn": "oWITGW_switchOn",
1083
- "block": "oWITGW_block",
1084
- "knob": "oWITGW_knob",
1085
- "endpointFields": "oWITGW_endpointFields",
1082
+ "headText": "oWITGW_headText",
1083
+ "info": "oWITGW_info",
1086
1084
  "row": "oWITGW_row",
1087
- "card": "oWITGW_card",
1088
- "endpointField": "oWITGW_endpointField",
1089
- "chevron": "oWITGW_chevron",
1090
- "switch": "oWITGW_switch",
1091
1085
  "body": "oWITGW_body",
1086
+ "block": "oWITGW_block",
1087
+ "nameRow": "oWITGW_nameRow",
1088
+ "chevron": "oWITGW_chevron",
1089
+ "chevronOpen": "oWITGW_chevronOpen",
1090
+ "cardOpen": "oWITGW_cardOpen",
1092
1091
  "blockTitle": "oWITGW_blockTitle",
1092
+ "endpointField": "oWITGW_endpointField",
1093
+ "header": "oWITGW_header",
1093
1094
  "description": "oWITGW_description",
1094
- "info": "oWITGW_info",
1095
- "name": "oWITGW_name",
1095
+ "endpointLabel": "oWITGW_endpointLabel",
1096
1096
  "rowLabel": "oWITGW_rowLabel",
1097
+ "switchOn": "oWITGW_switchOn",
1098
+ "knob": "oWITGW_knob",
1099
+ "endpointFields": "oWITGW_endpointFields",
1100
+ "name": "oWITGW_name",
1101
+ "saved": "oWITGW_saved",
1102
+ "switch": "oWITGW_switch",
1097
1103
  "errorText": "oWITGW_errorText",
1104
+ "card": "oWITGW_card",
1098
1105
  "hint": "oWITGW_hint",
1099
- "cardOpen": "oWITGW_cardOpen",
1100
- "endpointLabel": "oWITGW_endpointLabel",
1101
- "headText": "oWITGW_headText",
1102
1106
  "modelInput": "oWITGW_modelInput",
1103
- "modelSave": "oWITGW_modelSave",
1104
- "saved": "oWITGW_saved",
1105
- "nameRow": "oWITGW_nameRow",
1106
- "chevronOpen": "oWITGW_chevronOpen",
1107
- "header": "oWITGW_header"
1107
+ "modelSave": "oWITGW_modelSave"
1108
1108
  };
1109
1109
  //#endregion
1110
1110
  //#region src/client/SettingsCard.tsx
package/lib/index.js CHANGED
@@ -795,14 +795,15 @@ function codexAppServerItemToLine(item) {
795
795
  } : void 0;
796
796
  case "commandExecution": {
797
797
  const command = typeof item["command"] === "string" ? item.command : void 0;
798
- const output = typeof item["aggregatedOutput"] === "string" && item["aggregatedOutput"].trim() !== "" ? item["aggregatedOutput"] : void 0;
799
- if (command === void 0 && output === void 0) return void 0;
798
+ const outcome = commandOutcome(item["aggregatedOutput"], item["exitCode"], item["status"]);
799
+ if (command === void 0 && outcome.result === void 0) return void 0;
800
800
  return {
801
801
  kind: "tool",
802
802
  id: itemId ?? `codex-live-${randomUUID()}`,
803
803
  name: "Bash",
804
804
  ...command === void 0 ? {} : { args: command },
805
- ...output === void 0 ? {} : { result: output }
805
+ ...outcome.result === void 0 ? {} : { result: outcome.result },
806
+ ...outcome.failed ? { failed: true } : {}
806
807
  };
807
808
  }
808
809
  case "webSearch": return {
@@ -846,10 +847,20 @@ function codexAppServerItemToLine(item) {
846
847
  case "mcpToolCall": {
847
848
  const server = typeof item["server"] === "string" ? item.server : void 0;
848
849
  const tool = typeof item["tool"] === "string" ? item.tool : void 0;
850
+ const args = item["arguments"] === void 0 || item["arguments"] === null ? void 0 : typeof item["arguments"] === "string" ? item["arguments"] : JSON.stringify(item["arguments"]);
851
+ const outcome = item["result"];
852
+ const text = (Array.isArray(outcome?.content) ? outcome.content : []).filter((block) => typeof block === "object" && block !== null).map((block) => typeof block["text"] === "string" ? block["text"] : void 0).filter((entry) => entry !== void 0 && entry !== "").join("\n");
853
+ const error = item["error"];
854
+ const failure = typeof error?.message === "string" && error.message !== "" ? error.message : void 0;
849
855
  return {
850
856
  kind: "tool",
851
857
  id: itemId ?? `codex-live-${randomUUID()}`,
852
- name: server !== void 0 && tool !== void 0 ? `${server}/${tool}` : tool ?? "mcp"
858
+ name: server !== void 0 && tool !== void 0 ? `${server}/${tool}` : tool ?? "mcp",
859
+ ...args === void 0 || args === "" || args === "{}" ? {} : { args },
860
+ ...failure !== void 0 ? {
861
+ result: failure,
862
+ failed: true
863
+ } : text === "" ? {} : { result: text }
853
864
  };
854
865
  }
855
866
  case "plan": return typeof item["text"] === "string" && item["text"].trim() !== "" ? {
@@ -1376,7 +1387,7 @@ var CodexLiveDriver = class {
1376
1387
  if (rt.threadId === void 0) {
1377
1388
  const permissionParams = {
1378
1389
  approvalPolicy: "never",
1379
- sandbox: this.config.sandbox ?? "workspace-write"
1390
+ sandbox: this.config.sandbox ?? spec.sandbox ?? "workspace-write"
1380
1391
  };
1381
1392
  if (spec.resume === void 0) {
1382
1393
  const response = await rt.peer.request("thread/start", {
@@ -2121,6 +2132,58 @@ async function listCodexConfigModels(homeDir) {
2121
2132
  }
2122
2133
  return found;
2123
2134
  }
2135
+ //#endregion
2136
+ //#region lib/types/codex-cli-provider.js
2137
+ /**
2138
+ * One-shot Codex CLI subagent lifecycle: spawn `codex exec "<task>"` through
2139
+ * the subprocess seam under the harness's scoped home, capture its printed
2140
+ * response as the run output, and dispose to whole-tree quiescence. Mirrors
2141
+ * the kimi-cli provider: every accepted run is a fresh process and a fresh
2142
+ * codex session; there is no continuation across runs.
2143
+ *
2144
+ * The provider name `codex-local` avoids colliding with the official
2145
+ * `subagent-codex` package's provider name `codex` — a composition that
2146
+ * mounts both would fail loud with DUPLICATE_PROVIDER. The tool row in this
2147
+ * bundle's patch takes the official model-facing name `subagent_codex`
2148
+ * instead: the official preset row ships disabled, and the patch disables
2149
+ * it too (a deliberate user re-enable conflicts loud, by design).
2150
+ * @module @khorsheed/dsh-local-agent-codex/codex-cli-provider
2151
+ */
2152
+ /**
2153
+ * The synthesized result for a call whose result the stream never recorded
2154
+ * (a killed run, or a result-less item type like web_search_call). The
2155
+ * released-log contract resolves every advertised call before its step
2156
+ * closes, so an interrupted call folds with this error result. Same text as
2157
+ * the kimi provider's placeholder, for family consistency.
2158
+ */
2159
+ /**
2160
+ * A finished command's result text and failure. Codex reports a command that
2161
+ * printed nothing with an empty aggregated output — a crash (exit 139) or a
2162
+ * silent failure read as "no result" and the mirror wrote the interrupted
2163
+ * placeholder. A finished command (an exit code, or a completed/failed status)
2164
+ * always carries a result: its output, or "(无输出)", with a non-zero exit
2165
+ * code appended and the result marked failed. A command still running yields
2166
+ * none, so the fold keeps waiting for it.
2167
+ * @param output - the item's aggregated output.
2168
+ * @param exitCode - the item's exit code, when finished.
2169
+ * @param status - the item's status.
2170
+ * @returns the result text (undefined while running) and whether it failed.
2171
+ */
2172
+ function commandOutcome(output, exitCode, status) {
2173
+ const text = typeof output === "string" && output.trim() !== "" ? output : void 0;
2174
+ const code = typeof exitCode === "number" && Number.isFinite(exitCode) ? exitCode : void 0;
2175
+ const finished = code !== void 0 || status === "completed" || status === "failed";
2176
+ const failed = code !== void 0 && code !== 0 || status === "failed";
2177
+ if (!finished) return text === void 0 ? { failed: false } : {
2178
+ result: text,
2179
+ failed: false
2180
+ };
2181
+ const exit = code !== void 0 && code !== 0 ? `(退出码 ${code})` : status === "failed" ? "(执行失败)" : "";
2182
+ return {
2183
+ result: text === void 0 ? exit === "" ? "(无输出)" : `${exit.slice(0, -1)},无输出)` : exit === "" ? text : `${text}\n${exit}`,
2184
+ failed
2185
+ };
2186
+ }
2124
2187
  /** Quote one TOML basic string for the `-c` config override. */
2125
2188
  function tomlString(value) {
2126
2189
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
@@ -2133,6 +2196,24 @@ const DEFAULT_DISPOSE_GRACE_MS = 3e3;
2133
2196
  * provider-controlled error output only.
2134
2197
  */
2135
2198
  const CODEX_AUTH_FAILURE = /401 unauthorized|unauthorized|authentication failed|not logged in|access token/i;
2199
+ /**
2200
+ * A session's standing sandbox mode — the host's `sandbox/mode` vocabulary is
2201
+ * codex's own (`read-only` | `workspace-write` | `danger-full-access`). Reads
2202
+ * the delegating session first (the mode the user sees and changes), then
2203
+ * the child's inherited record.
2204
+ * @param ctx - context carrying the live session store.
2205
+ * @param childSession - the member's child session, when one exists.
2206
+ * @param parentSessionId - the delegating session.
2207
+ * @returns the mode, or undefined when neither session records one.
2208
+ */
2209
+ function sessionSandboxMode(ctx, childSession, parentSessionId) {
2210
+ const sessions = ctx.get("sessions");
2211
+ const parent = parentSessionId === void 0 ? void 0 : sessions?.get(SessionId(parentSessionId));
2212
+ for (const session of [parent, childSession]) {
2213
+ const mode = session?.snapshotEvents().filter((event) => event.type === "sandbox/mode").at(-1)?.data;
2214
+ if (mode?.mode === "read-only" || mode?.mode === "workspace-write" || mode?.mode === "danger-full-access") return mode.mode;
2215
+ }
2216
+ }
2136
2217
  /** Register one member run with the channel; see {@link CodexMemberRunHandle}. */
2137
2218
  function registerCodexMemberRun(ctx, childSessionId, parentSessionId) {
2138
2219
  const registry = ctx.localAgent;
@@ -2229,7 +2310,7 @@ var CodexCliProvider = class {
2229
2310
  * relay (a custom `model_providers` entry on the argv, the key in the
2230
2311
  * env); with no key the spawn carries no credential at all.
2231
2312
  */
2232
- constructor(ctx, sandbox = "workspace-write", live, model, overrides, endpoint) {
2313
+ constructor(ctx, sandbox = void 0, live, model, overrides, endpoint) {
2233
2314
  this.ctx = ctx;
2234
2315
  this.sandbox = sandbox;
2235
2316
  this.live = live;
@@ -2237,6 +2318,18 @@ var CodexCliProvider = class {
2237
2318
  this.overrides = overrides;
2238
2319
  this.endpoint = endpoint;
2239
2320
  }
2321
+ /**
2322
+ * The sandbox this round runs under: the plugin config's explicit policy
2323
+ * when set, else the member's session's own standing mode — the delegating
2324
+ * session's latest `sandbox/mode`, then the child's inherited one — so a
2325
+ * member runs with exactly the access its session shows (a full-access
2326
+ * session's Codex member was confined to workspace-write and Blender
2327
+ * crashed under the seatbelt). `workspace-write` when neither says.
2328
+ */
2329
+ sandboxFor(childSession, parentSessionId) {
2330
+ if (this.sandbox !== void 0) return this.sandbox;
2331
+ return sessionSandboxMode(this.ctx, childSession, parentSessionId) ?? "workspace-write";
2332
+ }
2240
2333
  /** The scope's endpoint credential for this round; absent resolver = never. */
2241
2334
  resolveEndpoint(homeDir) {
2242
2335
  return this.endpoint === void 0 ? Promise.resolve(void 0) : this.endpoint(homeDir).catch(() => void 0);
@@ -2327,6 +2420,7 @@ var CodexCliProvider = class {
2327
2420
  homeDir,
2328
2421
  childSession,
2329
2422
  parentSessionId: request.parent.session.id,
2423
+ sandbox: this.sandboxFor(childSession, request.parent.session.id),
2330
2424
  ...startModel === void 0 ? {} : { startModel },
2331
2425
  ...configuration === void 0 ? {} : { configuration: configuration.resolved },
2332
2426
  onThreadId: (threadId) => {
@@ -2357,7 +2451,7 @@ var CodexCliProvider = class {
2357
2451
  ...exec === void 0 ? {} : { exec },
2358
2452
  endpointLabel: baseUrl,
2359
2453
  endpointArgv: endpointConfigOverrides(endpoint),
2360
- sandbox: this.sandbox,
2454
+ sandbox: this.sandboxFor(childSession, request.parent.session.id),
2361
2455
  ...configuration?.resolved ?? resolveRoundModel(this.overrides?.(runId) ?? requestedModel, () => this.model?.(scope)),
2362
2456
  disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
2363
2457
  spawn: (spec) => this.ctx.subprocess.spawn(spec),
@@ -2416,6 +2510,7 @@ var CodexCliProvider = class {
2416
2510
  homeDir,
2417
2511
  childSession,
2418
2512
  parentSessionId: request.parent.session.id,
2513
+ sandbox: this.sandboxFor(childSession, request.parent.session.id),
2419
2514
  resume: {
2420
2515
  cliSessionId: intent.cliSessionId,
2421
2516
  turn: nextTurn
@@ -2446,7 +2541,7 @@ var CodexCliProvider = class {
2446
2541
  ...exec === void 0 ? {} : { exec },
2447
2542
  endpointLabel: baseUrl,
2448
2543
  endpointArgv: endpointConfigOverrides(endpoint),
2449
- sandbox: this.sandbox,
2544
+ sandbox: this.sandboxFor(childSession, request.parent.session.id),
2450
2545
  ...configuration?.resolved ?? resolveRoundModel(this.overrides?.(intent.childSessionId) ?? requestedModel, () => this.model?.(scope)),
2451
2546
  disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
2452
2547
  spawn: (spec) => this.ctx.subprocess.spawn(spec),
@@ -2574,13 +2669,14 @@ function foldCodexStreamLine(state, raw) {
2574
2669
  } else if (item.type === "command_execution") {
2575
2670
  countToolCall(state, "command_execution");
2576
2671
  const command = typeof item.command === "string" ? item.command : void 0;
2577
- const output = typeof item.aggregated_output === "string" && item.aggregated_output.trim() !== "" ? item.aggregated_output : void 0;
2578
- if (command !== void 0 || output !== void 0) state.lines.push({
2672
+ const outcome = commandOutcome(item.aggregated_output, item.exit_code, item.status);
2673
+ if (command !== void 0 || outcome.result !== void 0) state.lines.push({
2579
2674
  kind: "tool",
2580
2675
  id: typeof item.id === "string" ? item.id : `codex-tool-${state.lines.length}`,
2581
2676
  name: "Bash",
2582
2677
  ...command === void 0 ? {} : { args: command },
2583
- ...output === void 0 ? {} : { result: output }
2678
+ ...outcome.result === void 0 ? {} : { result: outcome.result },
2679
+ ...outcome.failed ? { failed: true } : {}
2584
2680
  });
2585
2681
  } else if (item.type === "web_search_call") {
2586
2682
  countToolCall(state, "web_search_call");
@@ -2976,7 +3072,7 @@ function appendCodexTranscriptLine(childSession, turn, step, line, usage) {
2976
3072
  type: "text",
2977
3073
  text: line.result ?? "(工具调用被中断,未能记录返回结果)"
2978
3074
  }],
2979
- isError: line.result === void 0
3075
+ isError: line.result === void 0 || line.failed === true
2980
3076
  })
2981
3077
  }, {
2982
3078
  surfaceOp: "append",
@@ -3998,7 +4094,7 @@ function apply(ctx, config) {
3998
4094
  }, CATALOG_WARMUP_DELAY_MS);
3999
4095
  warmup.unref();
4000
4096
  const liveSwitch = new LiveDriverSwitch(ctx, face, {
4001
- sandbox,
4097
+ sandbox: config.sandbox,
4002
4098
  model: (childSessionId) => memberModelOverrides.get(childSessionId) ?? resolveModel((ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.scope),
4003
4099
  endpoint: resolveEndpoint,
4004
4100
  ...config.liveIdleMs === void 0 ? {} : { liveIdleMs: config.liveIdleMs }
@@ -4020,7 +4116,7 @@ function apply(ctx, config) {
4020
4116
  catalog: (scopedHome, cwd) => modelCatalog.read(scopedHome, cwd),
4021
4117
  catalogDefault: (scopedHome, cwd) => modelCatalog.readDefault(scopedHome, cwd)
4022
4118
  });
4023
- const disposeProvider = ctx.subagents.registerProvider(new CodexCliProvider(ctx, sandbox, liveSwitch.resolve, resolveModel, (childSessionId) => memberModelOverrides.get(childSessionId), resolveEndpoint));
4119
+ const disposeProvider = ctx.subagents.registerProvider(new CodexCliProvider(ctx, config.sandbox, liveSwitch.resolve, resolveModel, (childSessionId) => memberModelOverrides.get(childSessionId), resolveEndpoint));
4024
4120
  const disposeHarness = ctx.localAgent.register({
4025
4121
  prepareMember: async ({ binding, childSession, configuration, signal }) => {
4026
4122
  const driver = liveSwitch.resolve(binding.childSessionId);
@@ -28,6 +28,23 @@ import { type CodexEndpointAuth } from './endpoint-auth.ts';
28
28
  * closes, so an interrupted call folds with this error result. Same text as
29
29
  * the kimi provider's placeholder, for family consistency.
30
30
  */
31
+ /**
32
+ * A finished command's result text and failure. Codex reports a command that
33
+ * printed nothing with an empty aggregated output — a crash (exit 139) or a
34
+ * silent failure read as "no result" and the mirror wrote the interrupted
35
+ * placeholder. A finished command (an exit code, or a completed/failed status)
36
+ * always carries a result: its output, or "(无输出)", with a non-zero exit
37
+ * code appended and the result marked failed. A command still running yields
38
+ * none, so the fold keeps waiting for it.
39
+ * @param output - the item's aggregated output.
40
+ * @param exitCode - the item's exit code, when finished.
41
+ * @param status - the item's status.
42
+ * @returns the result text (undefined while running) and whether it failed.
43
+ */
44
+ export declare function commandOutcome(output: unknown, exitCode: unknown, status: unknown): {
45
+ result?: string;
46
+ failed: boolean;
47
+ };
31
48
  export declare const CODEX_INTERRUPTED_TOOL_RESULT = "\uFF08\u5DE5\u5177\u8C03\u7528\u88AB\u4E2D\u65AD\uFF0C\u672A\u80FD\u8BB0\u5F55\u8FD4\u56DE\u7ED3\u679C\uFF09";
32
49
  /** Default POSIX grace between subprocess termination tiers. */
33
50
  export declare const DEFAULT_DISPOSE_GRACE_MS = 3000;
@@ -37,6 +54,17 @@ export declare const DEFAULT_DISPOSE_GRACE_MS = 3000;
37
54
  * provider-controlled error output only.
38
55
  */
39
56
  export declare const CODEX_AUTH_FAILURE: RegExp;
57
+ /**
58
+ * A session's standing sandbox mode — the host's `sandbox/mode` vocabulary is
59
+ * codex's own (`read-only` | `workspace-write` | `danger-full-access`). Reads
60
+ * the delegating session first (the mode the user sees and changes), then
61
+ * the child's inherited record.
62
+ * @param ctx - context carrying the live session store.
63
+ * @param childSession - the member's child session, when one exists.
64
+ * @param parentSessionId - the delegating session.
65
+ * @returns the mode, or undefined when neither session records one.
66
+ */
67
+ export declare function sessionSandboxMode(ctx: Context, childSession: Session | undefined, parentSessionId: string | undefined): CodexSandbox | undefined;
40
68
  /** Codex sandbox policy values accepted by `codex exec --sandbox`. */
41
69
  export type CodexSandbox = 'read-only' | 'workspace-write' | 'danger-full-access';
42
70
  /**
@@ -76,6 +104,7 @@ export declare function registerCodexMemberRun(ctx: Context, childSessionId: str
76
104
  export declare function codexCliVersion(ctx: Context, homeDir: string): Promise<string | undefined>;
77
105
  export declare class CodexCliProvider implements SubagentProvider {
78
106
  private readonly ctx;
107
+ /** An explicit policy from the plugin config; undefined follows the member's session. */
79
108
  private readonly sandbox;
80
109
  private readonly live?;
81
110
  private readonly model?;
@@ -107,7 +136,18 @@ export declare class CodexCliProvider implements SubagentProvider {
107
136
  * relay (a custom `model_providers` entry on the argv, the key in the
108
137
  * env); with no key the spawn carries no credential at all.
109
138
  */
110
- constructor(ctx: Context, sandbox?: CodexSandbox, live?: CodexLiveDriver | ((childSessionId: string) => CodexLiveDriver | undefined) | undefined, model?: ((scope?: string) => string | undefined) | undefined, overrides?: ((childSessionId: string) => string | undefined) | undefined, endpoint?: ((scopeHome: string) => Promise<CodexEndpointAuth>) | undefined);
139
+ constructor(ctx: Context,
140
+ /** An explicit policy from the plugin config; undefined follows the member's session. */
141
+ sandbox?: CodexSandbox | undefined, live?: CodexLiveDriver | ((childSessionId: string) => CodexLiveDriver | undefined) | undefined, model?: ((scope?: string) => string | undefined) | undefined, overrides?: ((childSessionId: string) => string | undefined) | undefined, endpoint?: ((scopeHome: string) => Promise<CodexEndpointAuth>) | undefined);
142
+ /**
143
+ * The sandbox this round runs under: the plugin config's explicit policy
144
+ * when set, else the member's session's own standing mode — the delegating
145
+ * session's latest `sandbox/mode`, then the child's inherited one — so a
146
+ * member runs with exactly the access its session shows (a full-access
147
+ * session's Codex member was confined to workspace-write and Blender
148
+ * crashed under the seatbelt). `workspace-write` when neither says.
149
+ */
150
+ sandboxFor(childSession: Session | undefined, parentSessionId: string | undefined): CodexSandbox;
111
151
  /** The scope's endpoint credential for this round; absent resolver = never. */
112
152
  private resolveEndpoint;
113
153
  /** Resolve the live driver for one round's member, if live is on for it. */
@@ -252,6 +292,7 @@ export type CodexTranscriptLine =
252
292
  name: string;
253
293
  args?: string;
254
294
  result?: string;
295
+ failed?: boolean;
255
296
  };
256
297
  /** Mutable fold state shared by the batch parse and the incremental parser. */
257
298
  interface CodexStreamFoldState {
@@ -30,6 +30,30 @@ import { codexRolloutRoundFacts, usageFromCodex } from './records.js';
30
30
  * closes, so an interrupted call folds with this error result. Same text as
31
31
  * the kimi provider's placeholder, for family consistency.
32
32
  */
33
+ /**
34
+ * A finished command's result text and failure. Codex reports a command that
35
+ * printed nothing with an empty aggregated output — a crash (exit 139) or a
36
+ * silent failure read as "no result" and the mirror wrote the interrupted
37
+ * placeholder. A finished command (an exit code, or a completed/failed status)
38
+ * always carries a result: its output, or "(无输出)", with a non-zero exit
39
+ * code appended and the result marked failed. A command still running yields
40
+ * none, so the fold keeps waiting for it.
41
+ * @param output - the item's aggregated output.
42
+ * @param exitCode - the item's exit code, when finished.
43
+ * @param status - the item's status.
44
+ * @returns the result text (undefined while running) and whether it failed.
45
+ */
46
+ export function commandOutcome(output, exitCode, status) {
47
+ const text = typeof output === 'string' && output.trim() !== '' ? output : undefined;
48
+ const code = typeof exitCode === 'number' && Number.isFinite(exitCode) ? exitCode : undefined;
49
+ const finished = code !== undefined || status === 'completed' || status === 'failed';
50
+ const failed = (code !== undefined && code !== 0) || status === 'failed';
51
+ if (!finished)
52
+ return text === undefined ? { failed: false } : { result: text, failed: false };
53
+ const exit = code !== undefined && code !== 0 ? `(退出码 ${code})` : status === 'failed' ? '(执行失败)' : '';
54
+ const result = text === undefined ? (exit === '' ? '(无输出)' : `${exit.slice(0, -1)},无输出)`) : exit === '' ? text : `${text}\n${exit}`;
55
+ return { result, failed };
56
+ }
33
57
  export const CODEX_INTERRUPTED_TOOL_RESULT = '(工具调用被中断,未能记录返回结果)';
34
58
  /** Quote one TOML basic string for the `-c` config override. */
35
59
  function tomlString(value) {
@@ -43,6 +67,26 @@ export const DEFAULT_DISPOSE_GRACE_MS = 3_000;
43
67
  * provider-controlled error output only.
44
68
  */
45
69
  export const CODEX_AUTH_FAILURE = /401 unauthorized|unauthorized|authentication failed|not logged in|access token/i;
70
+ /**
71
+ * A session's standing sandbox mode — the host's `sandbox/mode` vocabulary is
72
+ * codex's own (`read-only` | `workspace-write` | `danger-full-access`). Reads
73
+ * the delegating session first (the mode the user sees and changes), then
74
+ * the child's inherited record.
75
+ * @param ctx - context carrying the live session store.
76
+ * @param childSession - the member's child session, when one exists.
77
+ * @param parentSessionId - the delegating session.
78
+ * @returns the mode, or undefined when neither session records one.
79
+ */
80
+ export function sessionSandboxMode(ctx, childSession, parentSessionId) {
81
+ const sessions = ctx.get('sessions');
82
+ const parent = parentSessionId === undefined ? undefined : sessions?.get(SessionId(parentSessionId));
83
+ for (const session of [parent, childSession]) {
84
+ const mode = session?.snapshotEvents().filter(event => event.type === 'sandbox/mode').at(-1)?.data;
85
+ if (mode?.mode === 'read-only' || mode?.mode === 'workspace-write' || mode?.mode === 'danger-full-access')
86
+ return mode.mode;
87
+ }
88
+ return undefined;
89
+ }
46
90
  /** Register one member run with the channel; see {@link CodexMemberRunHandle}. */
47
91
  export function registerCodexMemberRun(ctx, childSessionId, parentSessionId) {
48
92
  const registry = ctx.localAgent;
@@ -155,7 +199,9 @@ export class CodexCliProvider {
155
199
  * relay (a custom `model_providers` entry on the argv, the key in the
156
200
  * env); with no key the spawn carries no credential at all.
157
201
  */
158
- constructor(ctx, sandbox = 'workspace-write', live, model, overrides, endpoint) {
202
+ constructor(ctx,
203
+ /** An explicit policy from the plugin config; undefined follows the member's session. */
204
+ sandbox = undefined, live, model, overrides, endpoint) {
159
205
  this.ctx = ctx;
160
206
  this.sandbox = sandbox;
161
207
  this.live = live;
@@ -163,6 +209,19 @@ export class CodexCliProvider {
163
209
  this.overrides = overrides;
164
210
  this.endpoint = endpoint;
165
211
  }
212
+ /**
213
+ * The sandbox this round runs under: the plugin config's explicit policy
214
+ * when set, else the member's session's own standing mode — the delegating
215
+ * session's latest `sandbox/mode`, then the child's inherited one — so a
216
+ * member runs with exactly the access its session shows (a full-access
217
+ * session's Codex member was confined to workspace-write and Blender
218
+ * crashed under the seatbelt). `workspace-write` when neither says.
219
+ */
220
+ sandboxFor(childSession, parentSessionId) {
221
+ if (this.sandbox !== undefined)
222
+ return this.sandbox;
223
+ return sessionSandboxMode(this.ctx, childSession, parentSessionId) ?? 'workspace-write';
224
+ }
166
225
  /** The scope's endpoint credential for this round; absent resolver = never. */
167
226
  resolveEndpoint(homeDir) {
168
227
  return this.endpoint === undefined ? Promise.resolve(undefined) : this.endpoint(homeDir).catch(() => undefined);
@@ -311,6 +370,7 @@ export class CodexCliProvider {
311
370
  homeDir,
312
371
  childSession,
313
372
  parentSessionId: request.parent.session.id,
373
+ sandbox: this.sandboxFor(childSession, request.parent.session.id),
314
374
  ...startModel === undefined ? {} : { startModel },
315
375
  ...configuration === undefined ? {} : { configuration: configuration.resolved },
316
376
  // The thread id arrives with thread/start (server-assigned), far
@@ -352,7 +412,7 @@ export class CodexCliProvider {
352
412
  ...exec === undefined ? {} : { exec },
353
413
  endpointLabel: baseUrl,
354
414
  endpointArgv: endpointConfigOverrides(endpoint),
355
- sandbox: this.sandbox,
415
+ sandbox: this.sandboxFor(childSession, request.parent.session.id),
356
416
  ...(configuration?.resolved ?? resolveRoundModel(this.overrides?.(runId) ?? requestedModel, () => this.model?.(scope))),
357
417
  disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
358
418
  spawn: spec => this.ctx.subprocess.spawn(spec),
@@ -441,6 +501,7 @@ export class CodexCliProvider {
441
501
  homeDir,
442
502
  childSession,
443
503
  parentSessionId: request.parent.session.id,
504
+ sandbox: this.sandboxFor(childSession, request.parent.session.id),
444
505
  resume: { cliSessionId: intent.cliSessionId, turn: nextTurn },
445
506
  ...startModel === undefined ? {} : { startModel },
446
507
  ...configuration === undefined ? {} : { configuration: configuration.resolved },
@@ -466,7 +527,7 @@ export class CodexCliProvider {
466
527
  ...exec === undefined ? {} : { exec },
467
528
  endpointLabel: baseUrl,
468
529
  endpointArgv: endpointConfigOverrides(endpoint),
469
- sandbox: this.sandbox,
530
+ sandbox: this.sandboxFor(childSession, request.parent.session.id),
470
531
  ...(configuration?.resolved ?? resolveRoundModel(this.overrides?.(intent.childSessionId) ?? requestedModel, () => this.model?.(scope))),
471
532
  disposeGraceMs: DEFAULT_DISPOSE_GRACE_MS,
472
533
  spawn: spec => this.ctx.subprocess.spawn(spec),
@@ -600,16 +661,15 @@ function foldCodexStreamLine(state, raw) {
600
661
  // command with neither text nor output is still a call the CLI made.
601
662
  countToolCall(state, 'command_execution');
602
663
  const command = typeof item.command === 'string' ? item.command : undefined;
603
- const output = typeof item.aggregated_output === 'string' && item.aggregated_output.trim() !== ''
604
- ? item.aggregated_output
605
- : undefined;
606
- if (command !== undefined || output !== undefined) {
664
+ const outcome = commandOutcome(item.aggregated_output, item.exit_code, item.status);
665
+ if (command !== undefined || outcome.result !== undefined) {
607
666
  state.lines.push({
608
667
  kind: 'tool',
609
668
  id: typeof item.id === 'string' ? item.id : `codex-tool-${state.lines.length}`,
610
669
  name: 'Bash',
611
670
  ...command === undefined ? {} : { args: command },
612
- ...output === undefined ? {} : { result: output },
671
+ ...outcome.result === undefined ? {} : { result: outcome.result },
672
+ ...outcome.failed ? { failed: true } : {},
613
673
  });
614
674
  }
615
675
  }
@@ -1095,7 +1155,7 @@ export function appendCodexTranscriptLine(childSession, turn, step, line, usage)
1095
1155
  message: createToolResultMessage({
1096
1156
  callId: line.id,
1097
1157
  content: [{ type: 'text', text: line.result ?? CODEX_INTERRUPTED_TOOL_RESULT }],
1098
- isError: line.result === undefined,
1158
+ isError: line.result === undefined || line.failed === true,
1099
1159
  }),
1100
1160
  }, { surfaceOp: 'append', sourceEventSeqs: [call.seq] });
1101
1161
  childSession.append('step/end', { turn, step });
@@ -28,7 +28,11 @@ export declare const CATALOG_WARMUP_DELAY_MS = 1500;
28
28
  export declare const inject: string[];
29
29
  /** Plugin config: the sandbox mode fresh delegations default to, plus the live driver. */
30
30
  export interface Config {
31
- /** Codex sandbox policy for `codex exec` / app-server threads; defaults to workspace-write. */
31
+ /**
32
+ * An explicit Codex sandbox policy for `codex exec` / app-server threads.
33
+ * Unset (the default), each member runs under its own session's sandbox
34
+ * mode — the access the session shows.
35
+ */
32
36
  sandbox?: 'read-only' | 'workspace-write' | 'danger-full-access';
33
37
  /**
34
38
  * Deployment-level endpoint credential: the relay's base URL (an
@@ -188,7 +188,8 @@ export function apply(ctx, config) {
188
188
  }, CATALOG_WARMUP_DELAY_MS);
189
189
  warmup.unref();
190
190
  const liveSwitch = new LiveDriverSwitch(ctx, face, {
191
- sandbox,
191
+ // Only an explicit policy overrides the member's session mode.
192
+ sandbox: config.sandbox,
192
193
  // The member's scope decides whose list answers: a named scope's
193
194
  // rounds read that scope's models.json, exactly like its endpoint
194
195
  // credential.
@@ -220,7 +221,7 @@ export function apply(ctx, config) {
220
221
  // cache.
221
222
  catalogDefault: (scopedHome, cwd) => modelCatalog.readDefault(scopedHome, cwd),
222
223
  });
223
- const disposeProvider = ctx.subagents.registerProvider(new CodexCliProvider(ctx, sandbox, liveSwitch.resolve, resolveModel, childSessionId => memberModelOverrides.get(childSessionId), resolveEndpoint));
224
+ const disposeProvider = ctx.subagents.registerProvider(new CodexCliProvider(ctx, config.sandbox, liveSwitch.resolve, resolveModel, childSessionId => memberModelOverrides.get(childSessionId), resolveEndpoint));
224
225
  const disposeHarness = ctx.localAgent.register({
225
226
  prepareMember: async ({ binding, childSession, configuration, signal }) => {
226
227
  const driver = liveSwitch.resolve(binding.childSessionId);
@@ -64,6 +64,8 @@ export interface CodexLiveRoundSpec {
64
64
  readonly childSession: Session;
65
65
  /** The delegating session (member-channel registration). */
66
66
  readonly parentSessionId: string;
67
+ /** The sandbox the member's thread starts under (its session's mode; see CodexCliProvider.sandboxFor). */
68
+ readonly sandbox?: 'read-only' | 'workspace-write' | 'danger-full-access';
67
69
  /**
68
70
  * Resume round: continue the thread recorded as `cliSessionId`, appending
69
71
  * under this turn number. Also the reattach path after a runtime crash.
@@ -27,7 +27,7 @@ import { createUserMessage } from '@deepseek-ai/dsh-llm';
27
27
  import { SessionId } from '@deepseek-ai/dsh-session';
28
28
  import { settleRunResult, subprocessRunHandle, } from '@deepseek-ai/dsh-subagent';
29
29
  import { delegationEnv, persistChildSession, LiveStreamPublisher, LiveFlush, LIVE_FLUSH_INTERVAL_MS } from '@khorsheed/dsh-local-agent';
30
- import { appendCodexRequestHeader, appendCodexTranscriptLine, codexAssistantEvent, codexLineText, DEFAULT_DISPOSE_GRACE_MS, registerCodexMemberRun, textTask, } from './codex-cli-provider.js';
30
+ import { appendCodexRequestHeader, appendCodexTranscriptLine, codexAssistantEvent, codexLineText, commandOutcome, DEFAULT_DISPOSE_GRACE_MS, registerCodexMemberRun, textTask, } from './codex-cli-provider.js';
31
31
  import { codexRolloutRoundFacts } from './records.js';
32
32
  import { endpointAuthEnv, endpointConfigOverrides } from './endpoint-auth.js';
33
33
  /** Default idle lifetime of an unused resident runtime before reclaim. */
@@ -99,17 +99,16 @@ export function codexAppServerItemToLine(item) {
99
99
  : undefined;
100
100
  case 'commandExecution': {
101
101
  const command = typeof item['command'] === 'string' ? item.command : undefined;
102
- const output = typeof item['aggregatedOutput'] === 'string' && item['aggregatedOutput'].trim() !== ''
103
- ? item['aggregatedOutput']
104
- : undefined;
105
- if (command === undefined && output === undefined)
102
+ const outcome = commandOutcome(item['aggregatedOutput'], item['exitCode'], item['status']);
103
+ if (command === undefined && outcome.result === undefined)
106
104
  return undefined;
107
105
  return {
108
106
  kind: 'tool',
109
107
  id: itemId ?? `codex-live-${randomUUID()}`,
110
108
  name: 'Bash',
111
109
  ...command === undefined ? {} : { args: command },
112
- ...output === undefined ? {} : { result: output },
110
+ ...outcome.result === undefined ? {} : { result: outcome.result },
111
+ ...outcome.failed ? { failed: true } : {},
113
112
  };
114
113
  }
115
114
  case 'webSearch':
@@ -162,12 +161,30 @@ export function codexAppServerItemToLine(item) {
162
161
  return { kind: 'tool', id: itemId ?? `codex-live-${randomUUID()}`, name: `collab/${tool}` };
163
162
  }
164
163
  case 'mcpToolCall': {
164
+ // An MCP call (the member bridge's room tools): {server, tool,
165
+ // arguments, status, result?: {content}, error?: {message}} — the
166
+ // arguments and the outcome ride the completed item; without them a
167
+ // member's ask_user_question read as a bare name.
165
168
  const server = typeof item['server'] === 'string' ? item.server : undefined;
166
169
  const tool = typeof item['tool'] === 'string' ? item.tool : undefined;
170
+ const args = item['arguments'] === undefined || item['arguments'] === null
171
+ ? undefined
172
+ : typeof item['arguments'] === 'string' ? item['arguments'] : JSON.stringify(item['arguments']);
173
+ const outcome = item['result'];
174
+ const content = Array.isArray(outcome?.content) ? outcome.content : [];
175
+ const text = content
176
+ .filter((block) => typeof block === 'object' && block !== null)
177
+ .map(block => typeof block['text'] === 'string' ? block['text'] : undefined)
178
+ .filter((entry) => entry !== undefined && entry !== '')
179
+ .join('\n');
180
+ const error = item['error'];
181
+ const failure = typeof error?.message === 'string' && error.message !== '' ? error.message : undefined;
167
182
  return {
168
183
  kind: 'tool',
169
184
  id: itemId ?? `codex-live-${randomUUID()}`,
170
185
  name: server !== undefined && tool !== undefined ? `${server}/${tool}` : tool ?? 'mcp',
186
+ ...args === undefined || args === '' || args === '{}' ? {} : { args },
187
+ ...failure !== undefined ? { result: failure, failed: true } : text === '' ? {} : { result: text },
171
188
  };
172
189
  }
173
190
  case 'plan':
@@ -719,7 +736,7 @@ export class CodexLiveDriver {
719
736
  if (rt.threadId === undefined) {
720
737
  const permissionParams = {
721
738
  approvalPolicy: 'never',
722
- sandbox: this.config.sandbox ?? 'workspace-write',
739
+ sandbox: this.config.sandbox ?? spec.sandbox ?? 'workspace-write',
723
740
  };
724
741
  if (spec.resume === undefined) {
725
742
  const response = await rt.peer.request('thread/start', {
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "local-agent",
9
9
  "codex"
10
10
  ],
11
- "version": "0.2.1",
11
+ "version": "0.2.2",
12
12
  "type": "module",
13
13
  "main": "lib/index.js",
14
14
  "types": "lib/types/index.d.ts",
@@ -69,7 +69,7 @@
69
69
  "dsh": ">=0.1.5-rc.1 <0.3.0-0"
70
70
  },
71
71
  "dependencies": {
72
- "@khorsheed/dsh-local-agent": "^0.2.1"
72
+ "@khorsheed/dsh-local-agent": "^0.2.2"
73
73
  },
74
74
  "peerDependencies": {
75
75
  "@deepseek-ai/cordis": "^4.0.1",