@cjhyy/code-shell-capability-coding 0.8.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.
Files changed (134) hide show
  1. package/README.md +69 -0
  2. package/THIRD_PARTY_NOTICES.md +206 -0
  3. package/dist/bin/agent-server-stdio.d.ts +1 -0
  4. package/dist/bin/agent-server-stdio.js +4 -0
  5. package/dist/capability-runtime.d.ts +22 -0
  6. package/dist/capability-runtime.js +88 -0
  7. package/dist/cc-orchestrator/agent-adapter.d.ts +55 -0
  8. package/dist/cc-orchestrator/agent-adapter.js +143 -0
  9. package/dist/cc-orchestrator/cc-capability.d.ts +20 -0
  10. package/dist/cc-orchestrator/cc-capability.js +57 -0
  11. package/dist/cc-orchestrator/codex-session-discovery.d.ts +45 -0
  12. package/dist/cc-orchestrator/codex-session-discovery.js +251 -0
  13. package/dist/cc-orchestrator/codex-session-history.d.ts +39 -0
  14. package/dist/cc-orchestrator/codex-session-history.js +249 -0
  15. package/dist/cc-orchestrator/codex-user-text.d.ts +9 -0
  16. package/dist/cc-orchestrator/codex-user-text.js +25 -0
  17. package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
  18. package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
  19. package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
  20. package/dist/cc-orchestrator/external-agent-changes.js +231 -0
  21. package/dist/cc-orchestrator/external-agent-driver.d.ts +19 -0
  22. package/dist/cc-orchestrator/external-agent-driver.js +284 -0
  23. package/dist/cc-orchestrator/external-agent-session-store.d.ts +36 -0
  24. package/dist/cc-orchestrator/external-agent-session-store.js +185 -0
  25. package/dist/cc-orchestrator/index.d.ts +10 -0
  26. package/dist/cc-orchestrator/index.js +10 -0
  27. package/dist/cc-orchestrator/related-session-discovery.d.ts +21 -0
  28. package/dist/cc-orchestrator/related-session-discovery.js +77 -0
  29. package/dist/cc-orchestrator/relevance-judge.d.ts +16 -0
  30. package/dist/cc-orchestrator/relevance-judge.js +38 -0
  31. package/dist/cc-orchestrator/session-discovery.d.ts +99 -0
  32. package/dist/cc-orchestrator/session-discovery.js +295 -0
  33. package/dist/cc-orchestrator/session-history.d.ts +55 -0
  34. package/dist/cc-orchestrator/session-history.js +150 -0
  35. package/dist/external-agents/config.d.ts +3 -0
  36. package/dist/external-agents/config.js +16 -0
  37. package/dist/external-agents/types.d.ts +32 -0
  38. package/dist/external-agents/types.js +1 -0
  39. package/dist/external-runtimes/claude-code/event-translator.d.ts +54 -0
  40. package/dist/external-runtimes/claude-code/event-translator.js +201 -0
  41. package/dist/external-runtimes/claude-code/mcp-config.d.ts +84 -0
  42. package/dist/external-runtimes/claude-code/mcp-config.js +175 -0
  43. package/dist/external-runtimes/claude-code/runtime.d.ts +56 -0
  44. package/dist/external-runtimes/claude-code/runtime.js +168 -0
  45. package/dist/external-runtimes/codex/app-server-client.d.ts +66 -0
  46. package/dist/external-runtimes/codex/app-server-client.js +244 -0
  47. package/dist/external-runtimes/codex/event-translator.d.ts +68 -0
  48. package/dist/external-runtimes/codex/event-translator.js +187 -0
  49. package/dist/external-runtimes/codex/index.d.ts +7 -0
  50. package/dist/external-runtimes/codex/index.js +6 -0
  51. package/dist/external-runtimes/codex/runtime.d.ts +128 -0
  52. package/dist/external-runtimes/codex/runtime.js +216 -0
  53. package/dist/external-runtimes/index.d.ts +37 -0
  54. package/dist/external-runtimes/index.js +27 -0
  55. package/dist/external-runtimes/session-factory.d.ts +97 -0
  56. package/dist/external-runtimes/session-factory.js +110 -0
  57. package/dist/external-runtimes/shared/mcp-bridge.d.ts +66 -0
  58. package/dist/external-runtimes/shared/mcp-bridge.js +365 -0
  59. package/dist/external-runtimes/shared/session-context-store.d.ts +83 -0
  60. package/dist/external-runtimes/shared/session-context-store.js +114 -0
  61. package/dist/external-runtimes/shared/spawn-env.d.ts +30 -0
  62. package/dist/external-runtimes/shared/spawn-env.js +41 -0
  63. package/dist/git/parse-log.d.ts +14 -0
  64. package/dist/git/parse-log.js +21 -0
  65. package/dist/git/utils.d.ts +49 -0
  66. package/dist/git/utils.js +161 -0
  67. package/dist/git/worktree/crud.d.ts +108 -0
  68. package/dist/git/worktree/crud.js +415 -0
  69. package/dist/git/worktree/diff.d.ts +14 -0
  70. package/dist/git/worktree/diff.js +83 -0
  71. package/dist/git/worktree/git-exec.d.ts +7 -0
  72. package/dist/git/worktree/git-exec.js +86 -0
  73. package/dist/git/worktree/index.d.ts +6 -0
  74. package/dist/git/worktree/index.js +6 -0
  75. package/dist/git/worktree/query.d.ts +43 -0
  76. package/dist/git/worktree/query.js +128 -0
  77. package/dist/git/worktree/slug.d.ts +12 -0
  78. package/dist/git/worktree/slug.js +59 -0
  79. package/dist/git/worktree.d.ts +1 -0
  80. package/dist/git/worktree.js +5 -0
  81. package/dist/index.capability.d.ts +7 -0
  82. package/dist/index.capability.js +190 -0
  83. package/dist/index.d.ts +18 -0
  84. package/dist/index.git.d.ts +4 -0
  85. package/dist/index.git.js +4 -0
  86. package/dist/index.js +18 -0
  87. package/dist/index.orchestration.d.ts +7 -0
  88. package/dist/index.orchestration.js +5 -0
  89. package/dist/lsp/client.d.ts +41 -0
  90. package/dist/lsp/client.js +192 -0
  91. package/dist/lsp/manager.d.ts +53 -0
  92. package/dist/lsp/manager.js +248 -0
  93. package/dist/lsp/root-path.d.ts +9 -0
  94. package/dist/lsp/root-path.js +12 -0
  95. package/dist/lsp/servers.d.ts +16 -0
  96. package/dist/lsp/servers.js +60 -0
  97. package/dist/prompt/coding.md +42 -0
  98. package/dist/quota/credentials.d.ts +3 -0
  99. package/dist/quota/credentials.js +80 -0
  100. package/dist/quota/index.d.ts +36 -0
  101. package/dist/quota/index.js +161 -0
  102. package/dist/quota/types.d.ts +48 -0
  103. package/dist/quota/types.js +13 -0
  104. package/dist/review/review-prompt.d.ts +28 -0
  105. package/dist/review/review-prompt.js +81 -0
  106. package/dist/tools/apply-patch/LICENSE-codex +201 -0
  107. package/dist/tools/apply-patch/NOTICE.md +29 -0
  108. package/dist/tools/apply-patch/applier.d.ts +26 -0
  109. package/dist/tools/apply-patch/applier.js +306 -0
  110. package/dist/tools/apply-patch/backup-targets.d.ts +10 -0
  111. package/dist/tools/apply-patch/backup-targets.js +30 -0
  112. package/dist/tools/apply-patch/eol.d.ts +4 -0
  113. package/dist/tools/apply-patch/eol.js +9 -0
  114. package/dist/tools/apply-patch/index.d.ts +20 -0
  115. package/dist/tools/apply-patch/index.js +106 -0
  116. package/dist/tools/apply-patch/parser.d.ts +17 -0
  117. package/dist/tools/apply-patch/parser.js +209 -0
  118. package/dist/tools/apply-patch/seek-sequence.d.ts +18 -0
  119. package/dist/tools/apply-patch/seek-sequence.js +123 -0
  120. package/dist/tools/apply-patch/types.d.ts +49 -0
  121. package/dist/tools/apply-patch/types.js +13 -0
  122. package/dist/tools/brief.d.ts +6 -0
  123. package/dist/tools/brief.js +41 -0
  124. package/dist/tools/check-quota.d.ts +14 -0
  125. package/dist/tools/check-quota.js +38 -0
  126. package/dist/tools/drive-agent.d.ts +52 -0
  127. package/dist/tools/drive-agent.js +1184 -0
  128. package/dist/tools/lsp.d.ts +6 -0
  129. package/dist/tools/lsp.js +220 -0
  130. package/dist/tools/notebook-edit.d.ts +6 -0
  131. package/dist/tools/notebook-edit.js +127 -0
  132. package/dist/tools/worktree.d.ts +10 -0
  133. package/dist/tools/worktree.js +363 -0
  134. package/package.json +60 -0
@@ -0,0 +1,187 @@
1
+ /** MCP server name CodeShell advertises its own tools under. */
2
+ const CODESHELL_MCP_SERVER = "codeshell_tools";
3
+ /** Cap on remembered finished turns — a session is long-lived, the set is not. */
4
+ const MAX_TOMBSTONES = 256;
5
+ function asRecord(value) {
6
+ return value && typeof value === "object" && !Array.isArray(value)
7
+ ? value
8
+ : undefined;
9
+ }
10
+ function str(value) {
11
+ return typeof value === "string" && value ? value : undefined;
12
+ }
13
+ /**
14
+ * Map a Codex turn status onto a CodeShell `TerminalReason`.
15
+ *
16
+ * `interrupted` must not become `completed`: a stopped turn that reports success
17
+ * makes downstream notifications ("done!") actively wrong.
18
+ */
19
+ function terminalReasonFor(status) {
20
+ switch (status) {
21
+ case "interrupted":
22
+ case "cancelled":
23
+ return "aborted_streaming";
24
+ case "failed":
25
+ return "model_error";
26
+ default:
27
+ return "completed";
28
+ }
29
+ }
30
+ export class CodexEventTranslator {
31
+ threadId;
32
+ codeshellServer;
33
+ turnNumber = 0;
34
+ activeTurnId;
35
+ /** Turns that reached a terminal state. Late events for these are dropped. */
36
+ finishedTurns = new Set();
37
+ constructor(options) {
38
+ this.threadId = options.threadId;
39
+ this.codeshellServer = options.codeshellServerName ?? CODESHELL_MCP_SERVER;
40
+ }
41
+ /**
42
+ * Translate one notification. Returns zero or more events — zero is a normal,
43
+ * common outcome (unknown method, other thread, stale turn), never an error.
44
+ *
45
+ * Deliberately total: the app-server is experimental and adds notifications
46
+ * between versions, so an unrecognised or malformed one must not take a session
47
+ * down.
48
+ */
49
+ translate(notification) {
50
+ const envelope = asRecord(notification);
51
+ if (!envelope)
52
+ return [];
53
+ const method = str(envelope.method);
54
+ const params = asRecord(envelope.params);
55
+ if (!method || !params)
56
+ return [];
57
+ // Thread scoping. `thread/started` is the documented exception: it carries
58
+ // the id at `params.thread.id` rather than the usual top-level field.
59
+ const nestedThread = asRecord(params.thread);
60
+ const threadId = str(params.threadId) ?? str(nestedThread?.id);
61
+ if (threadId && threadId !== this.threadId)
62
+ return [];
63
+ switch (method) {
64
+ case "turn/started":
65
+ return this.onTurnStarted(params);
66
+ case "turn/completed":
67
+ return this.onTurnCompleted(params);
68
+ case "error":
69
+ return this.onError(params);
70
+ case "item/agentMessage/delta":
71
+ return this.onAgentDelta(params);
72
+ case "item/started":
73
+ return this.onItemStarted(params);
74
+ case "item/completed":
75
+ return this.onItemCompleted(params);
76
+ default:
77
+ // Everything else (token usage, rate limits, plan updates, MCP status…)
78
+ // is either handled elsewhere or deliberately not surfaced.
79
+ return [];
80
+ }
81
+ }
82
+ /** True when an event belongs to a turn that has already finished. */
83
+ isStale(turnId) {
84
+ if (!turnId)
85
+ return false;
86
+ if (this.finishedTurns.has(turnId))
87
+ return true;
88
+ // A turn id we have never seen, while another turn is live, is a straggler
89
+ // from a superseded turn — not an implicit new turn.
90
+ return this.activeTurnId !== undefined && turnId !== this.activeTurnId;
91
+ }
92
+ remember(turnId) {
93
+ this.finishedTurns.add(turnId);
94
+ if (this.finishedTurns.size > MAX_TOMBSTONES) {
95
+ const oldest = this.finishedTurns.values().next().value;
96
+ if (oldest !== undefined)
97
+ this.finishedTurns.delete(oldest);
98
+ }
99
+ }
100
+ onTurnStarted(params) {
101
+ const turn = asRecord(params.turn);
102
+ const turnId = str(turn?.id) ?? str(params.turnId);
103
+ // A `turn/started` for an already-finished turn is an orphan (its RPC failed
104
+ // and the daemon created the turn anyway, or completion beat it here). It
105
+ // must not reactivate the session.
106
+ if (turnId && this.finishedTurns.has(turnId))
107
+ return [];
108
+ this.activeTurnId = turnId;
109
+ this.turnNumber += 1;
110
+ return [{ type: "stream_request_start", turnNumber: this.turnNumber }];
111
+ }
112
+ onTurnCompleted(params) {
113
+ const turn = asRecord(params.turn);
114
+ const turnId = str(turn?.id) ?? str(params.turnId);
115
+ // Duplicate delivery is expected; only the first completion closes the turn.
116
+ if (turnId && this.finishedTurns.has(turnId))
117
+ return [];
118
+ if (turnId)
119
+ this.remember(turnId);
120
+ this.activeTurnId = undefined;
121
+ return [{ type: "turn_complete", reason: terminalReasonFor(str(turn?.status)) }];
122
+ }
123
+ onError(params) {
124
+ // A retryable error is not terminal. Reporting completion here would close
125
+ // the turn in the UI while Codex is still working on it — and 401 retries in
126
+ // particular fire about once a second, so this would also storm.
127
+ if (params.willRetry === true)
128
+ return [];
129
+ const turnId = str(params.turnId);
130
+ if (turnId && this.finishedTurns.has(turnId))
131
+ return [];
132
+ if (turnId)
133
+ this.remember(turnId);
134
+ this.activeTurnId = undefined;
135
+ return [{ type: "turn_complete", reason: "model_error" }];
136
+ }
137
+ onAgentDelta(params) {
138
+ if (this.isStale(str(params.turnId)))
139
+ return [];
140
+ const delta = str(params.delta);
141
+ return delta ? [{ type: "text_delta", text: delta }] : [];
142
+ }
143
+ /**
144
+ * A CodeShell Host Tool call must not produce a card here — `ToolExecutor`
145
+ * already emits one, and two unsynchronised sources for one operation is
146
+ * exactly what §15.2 forbids. Only OUR server is suppressed; a third-party MCP
147
+ * server has no CodeShell-side lifecycle, so dropping it would lose the card.
148
+ */
149
+ isCodeshellHostTool(item) {
150
+ if (!item)
151
+ return false;
152
+ if (str(item.type) !== "mcpToolCall")
153
+ return false;
154
+ return str(item.server) === this.codeshellServer;
155
+ }
156
+ onItemStarted(params) {
157
+ if (this.isStale(str(params.turnId)))
158
+ return [];
159
+ const item = asRecord(params.item);
160
+ const id = str(item?.id);
161
+ const type = str(item?.type);
162
+ if (!item || !id || !type)
163
+ return [];
164
+ if (this.isCodeshellHostTool(item))
165
+ return [];
166
+ const { id: _id, type: _type, ...args } = item;
167
+ return [{ type: "tool_use_start", toolCall: { id, toolName: type, args } }];
168
+ }
169
+ onItemCompleted(params) {
170
+ if (this.isStale(str(params.turnId)))
171
+ return [];
172
+ const item = asRecord(params.item);
173
+ const id = str(item?.id);
174
+ const type = str(item?.type);
175
+ if (!item || !id || !type)
176
+ return [];
177
+ if (this.isCodeshellHostTool(item))
178
+ return [];
179
+ const output = str(item.aggregatedOutput) ?? str(item.output) ?? str(item.text) ?? str(item.result);
180
+ return [
181
+ {
182
+ type: "tool_result",
183
+ result: { id, toolName: type, ...(output !== undefined ? { result: output } : {}) },
184
+ },
185
+ ];
186
+ }
187
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Codex-specific pieces. The transport, the session store and the spawn env are
3
+ * shared with Claude Code (see `../shared/`) — both runtimes speak HTTP MCP with
4
+ * a bearer token, so there is one bridge implementation, not two.
5
+ */
6
+ export { CodexEventTranslator } from "./event-translator.js";
7
+ export type { CodexEventTranslatorOptions } from "./event-translator.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Codex-specific pieces. The transport, the session store and the spawn env are
3
+ * shared with Claude Code (see `../shared/`) — both runtimes speak HTTP MCP with
4
+ * a bearer token, so there is one bridge implementation, not two.
5
+ */
6
+ export { CodexEventTranslator } from "./event-translator.js";
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Codex as a CodeShell Agent Runtime.
3
+ *
4
+ * This is the piece that makes Codex usable as an execution backend rather than
5
+ * just a caller of CodeShell tools: it owns the app-server process, starts a
6
+ * thread, runs turns, translates notifications into `StreamEvent`, and wires the
7
+ * loopback MCP bridge so the model can reach CodeShell tools mid-turn.
8
+ *
9
+ * Deliberately NOT an `LLMClientBase` (§6.1, §22.1): Codex brings its own agent
10
+ * loop, tools, session and approval protocol. Wrapping it as a model client would
11
+ * nest two agent loops. This sits beside the Engine, not inside it.
12
+ *
13
+ * Timeouts follow §13.3's ordering and Cindy's measurements: the client has no
14
+ * global timeout (a late response is protocol-legal, and a global timeout
15
+ * manufactures orphan turns), so only the calls where hanging is worse than
16
+ * failing opt in.
17
+ */
18
+ import type { StreamEvent } from "@cjhyy/code-shell-core/extension";
19
+ import { type AppServerClientOptions } from "./app-server-client.js";
20
+ import { type McpBridgeHandle } from "../shared/mcp-bridge.js";
21
+ export interface CodexRuntimeOptions {
22
+ cwd: string;
23
+ /** CodeShell business session id — the authorization subject, never the thread id. */
24
+ businessSessionId: string;
25
+ /** Loopback MCP bridge the model reaches CodeShell tools through. */
26
+ bridge: McpBridgeHandle;
27
+ /**
28
+ * MCP server name CodeShell's tools are advertised under. Used to decide which
29
+ * approval elicitations are ours to accept — a name mismatch means the request
30
+ * belongs to some other server and must be declined.
31
+ */
32
+ bridgeServerName?: string;
33
+ model?: string;
34
+ /** Codex sandbox mode. Kebab-case per protocol (`workspace-write`, …). */
35
+ sandbox?: string;
36
+ /** Codex approval policy. Also kebab-case. */
37
+ approvalPolicy?: string;
38
+ client?: AppServerClientOptions;
39
+ log?: (event: string, data: Record<string, unknown>) => void;
40
+ }
41
+ export interface CodexTurnHandle {
42
+ /** Codex turn id, once known. */
43
+ readonly turnId: string | undefined;
44
+ /** Resolves when the turn reaches a terminal state. */
45
+ readonly done: Promise<void>;
46
+ }
47
+ /**
48
+ * How the host answers Codex's own approval requests for its NATIVE tools.
49
+ *
50
+ * CodeShell Host Tools are NOT routed here — they go through the MCP bridge and
51
+ * are authorized by `ToolExecutor` (§10.3/§11.4). Answering native approvals here
52
+ * as well is what keeps a single operation from being approved twice.
53
+ */
54
+ export type NativeApprovalDecision = "accept" | "acceptForSession" | "decline" | "cancel";
55
+ export interface CodexRuntimeHooks {
56
+ onEvent?: (event: StreamEvent) => void;
57
+ /** Called for Codex's native tool approvals. Defaults to `decline`. */
58
+ onNativeApproval?: (request: {
59
+ method: string;
60
+ params: unknown;
61
+ }) => Promise<NativeApprovalDecision> | NativeApprovalDecision;
62
+ }
63
+ export declare class CodexRuntime {
64
+ private readonly options;
65
+ private readonly hooks;
66
+ readonly kind: "codex";
67
+ private readonly client;
68
+ private readonly log;
69
+ private translator?;
70
+ private threadId?;
71
+ private started;
72
+ private activeTurn?;
73
+ constructor(options: CodexRuntimeOptions, hooks?: CodexRuntimeHooks);
74
+ /** Codex thread id, once the thread exists. Protocol routing only — never the
75
+ * authorization subject (§8.1). */
76
+ get runtimeSessionId(): string | undefined;
77
+ /**
78
+ * Spawn the app-server, handshake, and open a thread.
79
+ *
80
+ * Handler registration happens BEFORE `start()`: the server pushes
81
+ * notifications as soon as the transport is up, and registering later drops
82
+ * them (there is no readiness banner — the `initialize` reply is readiness).
83
+ */
84
+ start(): Promise<void>;
85
+ /**
86
+ * Run one turn. Resolves when the turn is terminal.
87
+ *
88
+ * A `turn/start` that times out does NOT mean the turn was not created — the
89
+ * daemon may have accepted it and lost the response. The translator's
90
+ * tombstones are what keep a late `turn/started` from reactivating a session
91
+ * that already reported terminal.
92
+ */
93
+ send(text: string): Promise<CodexTurnHandle>;
94
+ /**
95
+ * Interrupt the active turn.
96
+ *
97
+ * Bounded on purpose: a hung app-server would otherwise leave this RPC pending
98
+ * forever, which silently defeats a stop request. Note what interrupt does NOT
99
+ * do — it cannot retract side effects already dispatched, so it promises only
100
+ * that no further step starts (§13.3).
101
+ */
102
+ interrupt(): Promise<void>;
103
+ close(): Promise<void>;
104
+ private onNotification;
105
+ /**
106
+ * Codex's native-tool approvals. CodeShell Host Tools never arrive here —
107
+ * they come in over the MCP bridge and are authorized by `ToolExecutor`, which
108
+ * is precisely how the same operation avoids being approved twice.
109
+ *
110
+ * Default is `decline`: an unanswered or unknown approval must not become an
111
+ * implicit yes.
112
+ */
113
+ private onServerRequest;
114
+ /**
115
+ * Accept an approval elicitation for CodeShell's own MCP server, and only that.
116
+ *
117
+ * This is the §10.3/§11.4 "no double approval" rule in practice: the call is
118
+ * about to reach `SessionToolHost` → `ToolExecutor`, which applies the exposure
119
+ * allowlist, the permission rules and the approval backend. Asking the user here
120
+ * as well would prompt twice for one operation; declining here would make
121
+ * CodeShell tools permanently unreachable.
122
+ *
123
+ * Everything else is declined. An elicitation from a THIRD-PARTY MCP server, or
124
+ * one whose kind we do not recognise, is not ours to consent to — and a blanket
125
+ * accept here would hand the runtime a yes for any server it can name.
126
+ */
127
+ private answerMcpElicitation;
128
+ }
@@ -0,0 +1,216 @@
1
+ import { CodexAppServerClient } from "./app-server-client.js";
2
+ import { CodexEventTranslator } from "./event-translator.js";
3
+ import { buildRuntimeSpawnEnv } from "../shared/spawn-env.js";
4
+ import { codexBridgeConfigArgs } from "../shared/mcp-bridge.js";
5
+ /** A thread/start or turn/start that hangs is worse than one that fails. */
6
+ const CRITICAL_RPC_TIMEOUT_MS = 60_000;
7
+ /** Interrupt is a fail-safe; an unbounded wait silently defeats it. */
8
+ const INTERRUPT_TIMEOUT_MS = 10_000;
9
+ export class CodexRuntime {
10
+ options;
11
+ hooks;
12
+ kind = "codex";
13
+ client;
14
+ log;
15
+ translator;
16
+ threadId;
17
+ started = false;
18
+ activeTurn;
19
+ constructor(options, hooks = {}) {
20
+ this.options = options;
21
+ this.hooks = hooks;
22
+ this.log = options.log ?? (() => { });
23
+ this.client = new CodexAppServerClient({
24
+ ...options.client,
25
+ cwd: options.cwd,
26
+ // Loopback must bypass any HTTP proxy, or Codex's Rust MCP client routes
27
+ // 127.0.0.1 through it and every tool call dies as UnexpectedContentType.
28
+ env: buildRuntimeSpawnEnv({
29
+ base: options.client?.env,
30
+ bridgeToken: { name: options.bridge.tokenEnvVar, value: options.bridge.token },
31
+ }),
32
+ // Point the thread at the bridge. The token travels by env var, not argv.
33
+ args: [...(options.client?.args ?? ["app-server"]), ...codexBridgeConfigArgs(options.bridge)],
34
+ log: this.log,
35
+ });
36
+ }
37
+ /** Codex thread id, once the thread exists. Protocol routing only — never the
38
+ * authorization subject (§8.1). */
39
+ get runtimeSessionId() {
40
+ return this.threadId;
41
+ }
42
+ /**
43
+ * Spawn the app-server, handshake, and open a thread.
44
+ *
45
+ * Handler registration happens BEFORE `start()`: the server pushes
46
+ * notifications as soon as the transport is up, and registering later drops
47
+ * them (there is no readiness banner — the `initialize` reply is readiness).
48
+ */
49
+ async start() {
50
+ if (this.started)
51
+ return;
52
+ this.started = true;
53
+ this.client.onNotification((method, params) => this.onNotification(method, params));
54
+ this.client.onServerRequest((method, params) => this.onServerRequest(method, params));
55
+ this.client.start();
56
+ await this.client.request("initialize", {
57
+ clientInfo: { name: "codeshell", title: "CodeShell", version: "1" },
58
+ capabilities: { experimentalApi: true },
59
+ }, CRITICAL_RPC_TIMEOUT_MS);
60
+ const thread = (await this.client.request("thread/start", {
61
+ cwd: this.options.cwd,
62
+ ...(this.options.model ? { model: this.options.model } : {}),
63
+ ...(this.options.sandbox ? { sandbox: this.options.sandbox } : {}),
64
+ ...(this.options.approvalPolicy ? { approvalPolicy: this.options.approvalPolicy } : {}),
65
+ }, CRITICAL_RPC_TIMEOUT_MS));
66
+ const threadId = thread?.thread?.id;
67
+ if (!threadId)
68
+ throw new Error("thread/start returned no thread id");
69
+ this.threadId = threadId;
70
+ this.translator = new CodexEventTranslator({
71
+ threadId,
72
+ sessionId: this.options.businessSessionId,
73
+ });
74
+ this.log("runtime.thread_started", {
75
+ businessSessionId: this.options.businessSessionId,
76
+ threadIdPrefix: threadId.slice(0, 8),
77
+ });
78
+ }
79
+ /**
80
+ * Run one turn. Resolves when the turn is terminal.
81
+ *
82
+ * A `turn/start` that times out does NOT mean the turn was not created — the
83
+ * daemon may have accepted it and lost the response. The translator's
84
+ * tombstones are what keep a late `turn/started` from reactivating a session
85
+ * that already reported terminal.
86
+ */
87
+ async send(text) {
88
+ if (!this.threadId)
89
+ throw new Error("CodexRuntime.send() before start()");
90
+ let resolveDone;
91
+ const done = new Promise((resolve) => (resolveDone = resolve));
92
+ this.activeTurn = { resolve: resolveDone };
93
+ const response = (await this.client.request("turn/start", {
94
+ threadId: this.threadId,
95
+ // `text_elements` is required by the protocol even when empty.
96
+ input: [{ type: "text", text, text_elements: [] }],
97
+ }, CRITICAL_RPC_TIMEOUT_MS));
98
+ const turnId = response?.turn?.id;
99
+ if (this.activeTurn)
100
+ this.activeTurn.id = turnId;
101
+ return {
102
+ get turnId() {
103
+ return turnId;
104
+ },
105
+ done,
106
+ };
107
+ }
108
+ /**
109
+ * Interrupt the active turn.
110
+ *
111
+ * Bounded on purpose: a hung app-server would otherwise leave this RPC pending
112
+ * forever, which silently defeats a stop request. Note what interrupt does NOT
113
+ * do — it cannot retract side effects already dispatched, so it promises only
114
+ * that no further step starts (§13.3).
115
+ */
116
+ async interrupt() {
117
+ const turnId = this.activeTurn?.id;
118
+ if (!this.threadId || !turnId)
119
+ return;
120
+ try {
121
+ await this.client.request("turn/interrupt", { threadId: this.threadId, turnId }, INTERRUPT_TIMEOUT_MS);
122
+ }
123
+ catch (error) {
124
+ // Surface rather than swallow: a stop the user asked for that did not land
125
+ // must not look like it did.
126
+ this.log("runtime.interrupt_failed", {
127
+ error: error instanceof Error ? error.message.slice(0, 200) : "unknown",
128
+ });
129
+ throw error;
130
+ }
131
+ }
132
+ async close() {
133
+ // Settle any waiter first, so a close during a live turn cannot leave the
134
+ // caller awaiting forever (§13.4).
135
+ this.activeTurn?.resolve();
136
+ this.activeTurn = undefined;
137
+ await this.client.close();
138
+ }
139
+ onNotification(method, params) {
140
+ const events = this.translator?.translate({ method, params }) ?? [];
141
+ for (const event of events) {
142
+ try {
143
+ this.hooks.onEvent?.(event);
144
+ }
145
+ catch (error) {
146
+ this.log("runtime.event_handler_failed", {
147
+ error: error instanceof Error ? error.name : "unknown",
148
+ });
149
+ }
150
+ if (event.type === "turn_complete") {
151
+ this.activeTurn?.resolve();
152
+ this.activeTurn = undefined;
153
+ }
154
+ }
155
+ }
156
+ /**
157
+ * Codex's native-tool approvals. CodeShell Host Tools never arrive here —
158
+ * they come in over the MCP bridge and are authorized by `ToolExecutor`, which
159
+ * is precisely how the same operation avoids being approved twice.
160
+ *
161
+ * Default is `decline`: an unanswered or unknown approval must not become an
162
+ * implicit yes.
163
+ */
164
+ async onServerRequest(method, params) {
165
+ // Codex asks for approval of OUR OWN MCP server's tools through
166
+ // `mcpServer/elicitation/request`, NOT through `item/permissions/requestApproval`.
167
+ // Measured: leaving it unhandled makes the app-server log
168
+ // `unhandled: mcpServer/elicitation/request` and the model report the tool call
169
+ // as "rejected" — so the reverse channel silently never fires. The distinguishing
170
+ // marker is `_meta.codex_approval_kind === "mcp_tool_call"`.
171
+ if (method === "mcpServer/elicitation/request") {
172
+ return this.answerMcpElicitation(params);
173
+ }
174
+ if (!method.includes("requestApproval")) {
175
+ // Some other server request (user input, …). Leave unhandled so the client
176
+ // answers method-not-found rather than inventing consent.
177
+ return undefined;
178
+ }
179
+ const decision = this.hooks.onNativeApproval
180
+ ? await this.hooks.onNativeApproval({ method, params })
181
+ : "decline";
182
+ this.log("runtime.native_approval", { method, decision });
183
+ return { decision };
184
+ }
185
+ /**
186
+ * Accept an approval elicitation for CodeShell's own MCP server, and only that.
187
+ *
188
+ * This is the §10.3/§11.4 "no double approval" rule in practice: the call is
189
+ * about to reach `SessionToolHost` → `ToolExecutor`, which applies the exposure
190
+ * allowlist, the permission rules and the approval backend. Asking the user here
191
+ * as well would prompt twice for one operation; declining here would make
192
+ * CodeShell tools permanently unreachable.
193
+ *
194
+ * Everything else is declined. An elicitation from a THIRD-PARTY MCP server, or
195
+ * one whose kind we do not recognise, is not ours to consent to — and a blanket
196
+ * accept here would hand the runtime a yes for any server it can name.
197
+ */
198
+ answerMcpElicitation(params) {
199
+ const record = params && typeof params === "object" ? params : {};
200
+ const meta = record._meta && typeof record._meta === "object"
201
+ ? record._meta
202
+ : {};
203
+ const kind = typeof meta.codex_approval_kind === "string" ? meta.codex_approval_kind : undefined;
204
+ const server = typeof record.serverName === "string" ? record.serverName : undefined;
205
+ const expected = this.options.bridgeServerName ?? "codeshell_tools";
206
+ const ours = kind === "mcp_tool_call" && server === expected;
207
+ this.log("runtime.mcp_elicitation", {
208
+ kind,
209
+ serverName: server,
210
+ decision: ours ? "accept" : "decline",
211
+ });
212
+ // `content`/`_meta` null: no form input, and no session-level persistence —
213
+ // each call is authorized on its own merits by ToolExecutor.
214
+ return { action: ours ? "accept" : "decline", content: null, _meta: null };
215
+ }
216
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * External Agent Runtimes: driving Claude Code / Codex as execution backends and
3
+ * letting them call CodeShell tools back through one loopback MCP bridge.
4
+ *
5
+ * The reverse tool channel is runtime-AGNOSTIC (`shared/`). That was not the
6
+ * original plan — the design assumed Claude Code would need an in-process MCP
7
+ * server via `@anthropic-ai/claude-agent-sdk`. It turns out `claude --mcp-config`
8
+ * accepts an HTTP MCP server with an `Authorization` header, exactly as Codex
9
+ * does, so both runtimes share one transport and CodeShell takes on no new
10
+ * dependency. Verified end-to-end against real `codex-cli 0.145.0` and real
11
+ * `claude 2.1.220` — see `docs/todo/evidence/`.
12
+ *
13
+ * The one real difference: Codex injects a per-call `_meta.threadId`, so a single
14
+ * bridge can serve many concurrent threads. Claude Code sends no equivalent, so
15
+ * its bridge is pinned to one session and the PORT is the attribution.
16
+ */
17
+ export { CODEX_MCP_TOKEN_ENV_VAR, codexBridgeConfigArgs, startLoopbackMcpBridge, threadIdFromMeta, } from "./shared/mcp-bridge.js";
18
+ export type { BridgeToolHost, McpBridgeHandle, McpBridgeOptions } from "./shared/mcp-bridge.js";
19
+ export { SessionContextStore } from "./shared/session-context-store.js";
20
+ export type { ResolveRequest, SessionContextResult, SessionContextMissReason, ToolHostRef, } from "./shared/session-context-store.js";
21
+ export { buildRuntimeSpawnEnv } from "./shared/spawn-env.js";
22
+ export type { RuntimeSpawnEnvOptions } from "./shared/spawn-env.js";
23
+ export { CodexEventTranslator } from "./codex/event-translator.js";
24
+ export { CodexAppServerClient } from "./codex/app-server-client.js";
25
+ export type { AppServerClientOptions } from "./codex/app-server-client.js";
26
+ export { CodexRuntime } from "./codex/runtime.js";
27
+ export type { CodexRuntimeOptions, CodexRuntimeHooks, CodexTurnHandle, NativeApprovalDecision, } from "./codex/runtime.js";
28
+ export { buildClaudeMcpConfig, claudeAllowedToolNames, claudeBridgeArgs, CLAUDE_MCP_SERVER_NAME, } from "./claude-code/mcp-config.js";
29
+ export type { ClaudeMcpConfigOptions, ClaudeMcpConfigFile } from "./claude-code/mcp-config.js";
30
+ export { writeClaudeMcpConfigFile } from "./claude-code/mcp-config.js";
31
+ export { ClaudeEventTranslator } from "./claude-code/event-translator.js";
32
+ export type { ClaudeEventTranslatorOptions } from "./claude-code/event-translator.js";
33
+ export { ClaudeCodeRuntime } from "./claude-code/runtime.js";
34
+ export type { ClaudeRuntimeOptions, ClaudeRuntimeHooks, ClaudeTurnHandle, } from "./claude-code/runtime.js";
35
+ export type { CodexEventTranslatorOptions } from "./codex/event-translator.js";
36
+ export { startExternalRuntimeSession } from "./session-factory.js";
37
+ export type { ExternalRuntimeKind, ExternalRuntimeSession, ExternalRuntimeSessionOptions, } from "./session-factory.js";
@@ -0,0 +1,27 @@
1
+ /**
2
+ * External Agent Runtimes: driving Claude Code / Codex as execution backends and
3
+ * letting them call CodeShell tools back through one loopback MCP bridge.
4
+ *
5
+ * The reverse tool channel is runtime-AGNOSTIC (`shared/`). That was not the
6
+ * original plan — the design assumed Claude Code would need an in-process MCP
7
+ * server via `@anthropic-ai/claude-agent-sdk`. It turns out `claude --mcp-config`
8
+ * accepts an HTTP MCP server with an `Authorization` header, exactly as Codex
9
+ * does, so both runtimes share one transport and CodeShell takes on no new
10
+ * dependency. Verified end-to-end against real `codex-cli 0.145.0` and real
11
+ * `claude 2.1.220` — see `docs/todo/evidence/`.
12
+ *
13
+ * The one real difference: Codex injects a per-call `_meta.threadId`, so a single
14
+ * bridge can serve many concurrent threads. Claude Code sends no equivalent, so
15
+ * its bridge is pinned to one session and the PORT is the attribution.
16
+ */
17
+ export { CODEX_MCP_TOKEN_ENV_VAR, codexBridgeConfigArgs, startLoopbackMcpBridge, threadIdFromMeta, } from "./shared/mcp-bridge.js";
18
+ export { SessionContextStore } from "./shared/session-context-store.js";
19
+ export { buildRuntimeSpawnEnv } from "./shared/spawn-env.js";
20
+ export { CodexEventTranslator } from "./codex/event-translator.js";
21
+ export { CodexAppServerClient } from "./codex/app-server-client.js";
22
+ export { CodexRuntime } from "./codex/runtime.js";
23
+ export { buildClaudeMcpConfig, claudeAllowedToolNames, claudeBridgeArgs, CLAUDE_MCP_SERVER_NAME, } from "./claude-code/mcp-config.js";
24
+ export { writeClaudeMcpConfigFile } from "./claude-code/mcp-config.js";
25
+ export { ClaudeEventTranslator } from "./claude-code/event-translator.js";
26
+ export { ClaudeCodeRuntime } from "./claude-code/runtime.js";
27
+ export { startExternalRuntimeSession } from "./session-factory.js";