@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.
- package/README.md +69 -0
- package/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/bin/agent-server-stdio.d.ts +1 -0
- package/dist/bin/agent-server-stdio.js +4 -0
- package/dist/capability-runtime.d.ts +22 -0
- package/dist/capability-runtime.js +88 -0
- package/dist/cc-orchestrator/agent-adapter.d.ts +55 -0
- package/dist/cc-orchestrator/agent-adapter.js +143 -0
- package/dist/cc-orchestrator/cc-capability.d.ts +20 -0
- package/dist/cc-orchestrator/cc-capability.js +57 -0
- package/dist/cc-orchestrator/codex-session-discovery.d.ts +45 -0
- package/dist/cc-orchestrator/codex-session-discovery.js +251 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +39 -0
- package/dist/cc-orchestrator/codex-session-history.js +249 -0
- package/dist/cc-orchestrator/codex-user-text.d.ts +9 -0
- package/dist/cc-orchestrator/codex-user-text.js +25 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
- package/dist/cc-orchestrator/external-agent-changes.js +231 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +19 -0
- package/dist/cc-orchestrator/external-agent-driver.js +284 -0
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +36 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +185 -0
- package/dist/cc-orchestrator/index.d.ts +10 -0
- package/dist/cc-orchestrator/index.js +10 -0
- package/dist/cc-orchestrator/related-session-discovery.d.ts +21 -0
- package/dist/cc-orchestrator/related-session-discovery.js +77 -0
- package/dist/cc-orchestrator/relevance-judge.d.ts +16 -0
- package/dist/cc-orchestrator/relevance-judge.js +38 -0
- package/dist/cc-orchestrator/session-discovery.d.ts +99 -0
- package/dist/cc-orchestrator/session-discovery.js +295 -0
- package/dist/cc-orchestrator/session-history.d.ts +55 -0
- package/dist/cc-orchestrator/session-history.js +150 -0
- package/dist/external-agents/config.d.ts +3 -0
- package/dist/external-agents/config.js +16 -0
- package/dist/external-agents/types.d.ts +32 -0
- package/dist/external-agents/types.js +1 -0
- package/dist/external-runtimes/claude-code/event-translator.d.ts +54 -0
- package/dist/external-runtimes/claude-code/event-translator.js +201 -0
- package/dist/external-runtimes/claude-code/mcp-config.d.ts +84 -0
- package/dist/external-runtimes/claude-code/mcp-config.js +175 -0
- package/dist/external-runtimes/claude-code/runtime.d.ts +56 -0
- package/dist/external-runtimes/claude-code/runtime.js +168 -0
- package/dist/external-runtimes/codex/app-server-client.d.ts +66 -0
- package/dist/external-runtimes/codex/app-server-client.js +244 -0
- package/dist/external-runtimes/codex/event-translator.d.ts +68 -0
- package/dist/external-runtimes/codex/event-translator.js +187 -0
- package/dist/external-runtimes/codex/index.d.ts +7 -0
- package/dist/external-runtimes/codex/index.js +6 -0
- package/dist/external-runtimes/codex/runtime.d.ts +128 -0
- package/dist/external-runtimes/codex/runtime.js +216 -0
- package/dist/external-runtimes/index.d.ts +37 -0
- package/dist/external-runtimes/index.js +27 -0
- package/dist/external-runtimes/session-factory.d.ts +97 -0
- package/dist/external-runtimes/session-factory.js +110 -0
- package/dist/external-runtimes/shared/mcp-bridge.d.ts +66 -0
- package/dist/external-runtimes/shared/mcp-bridge.js +365 -0
- package/dist/external-runtimes/shared/session-context-store.d.ts +83 -0
- package/dist/external-runtimes/shared/session-context-store.js +114 -0
- package/dist/external-runtimes/shared/spawn-env.d.ts +30 -0
- package/dist/external-runtimes/shared/spawn-env.js +41 -0
- package/dist/git/parse-log.d.ts +14 -0
- package/dist/git/parse-log.js +21 -0
- package/dist/git/utils.d.ts +49 -0
- package/dist/git/utils.js +161 -0
- package/dist/git/worktree/crud.d.ts +108 -0
- package/dist/git/worktree/crud.js +415 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +83 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +86 -0
- package/dist/git/worktree/index.d.ts +6 -0
- package/dist/git/worktree/index.js +6 -0
- package/dist/git/worktree/query.d.ts +43 -0
- package/dist/git/worktree/query.js +128 -0
- package/dist/git/worktree/slug.d.ts +12 -0
- package/dist/git/worktree/slug.js +59 -0
- package/dist/git/worktree.d.ts +1 -0
- package/dist/git/worktree.js +5 -0
- package/dist/index.capability.d.ts +7 -0
- package/dist/index.capability.js +190 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.git.d.ts +4 -0
- package/dist/index.git.js +4 -0
- package/dist/index.js +18 -0
- package/dist/index.orchestration.d.ts +7 -0
- package/dist/index.orchestration.js +5 -0
- package/dist/lsp/client.d.ts +41 -0
- package/dist/lsp/client.js +192 -0
- package/dist/lsp/manager.d.ts +53 -0
- package/dist/lsp/manager.js +248 -0
- package/dist/lsp/root-path.d.ts +9 -0
- package/dist/lsp/root-path.js +12 -0
- package/dist/lsp/servers.d.ts +16 -0
- package/dist/lsp/servers.js +60 -0
- package/dist/prompt/coding.md +42 -0
- package/dist/quota/credentials.d.ts +3 -0
- package/dist/quota/credentials.js +80 -0
- package/dist/quota/index.d.ts +36 -0
- package/dist/quota/index.js +161 -0
- package/dist/quota/types.d.ts +48 -0
- package/dist/quota/types.js +13 -0
- package/dist/review/review-prompt.d.ts +28 -0
- package/dist/review/review-prompt.js +81 -0
- package/dist/tools/apply-patch/LICENSE-codex +201 -0
- package/dist/tools/apply-patch/NOTICE.md +29 -0
- package/dist/tools/apply-patch/applier.d.ts +26 -0
- package/dist/tools/apply-patch/applier.js +306 -0
- package/dist/tools/apply-patch/backup-targets.d.ts +10 -0
- package/dist/tools/apply-patch/backup-targets.js +30 -0
- package/dist/tools/apply-patch/eol.d.ts +4 -0
- package/dist/tools/apply-patch/eol.js +9 -0
- package/dist/tools/apply-patch/index.d.ts +20 -0
- package/dist/tools/apply-patch/index.js +106 -0
- package/dist/tools/apply-patch/parser.d.ts +17 -0
- package/dist/tools/apply-patch/parser.js +209 -0
- package/dist/tools/apply-patch/seek-sequence.d.ts +18 -0
- package/dist/tools/apply-patch/seek-sequence.js +123 -0
- package/dist/tools/apply-patch/types.d.ts +49 -0
- package/dist/tools/apply-patch/types.js +13 -0
- package/dist/tools/brief.d.ts +6 -0
- package/dist/tools/brief.js +41 -0
- package/dist/tools/check-quota.d.ts +14 -0
- package/dist/tools/check-quota.js +38 -0
- package/dist/tools/drive-agent.d.ts +52 -0
- package/dist/tools/drive-agent.js +1184 -0
- package/dist/tools/lsp.d.ts +6 -0
- package/dist/tools/lsp.js +220 -0
- package/dist/tools/notebook-edit.d.ts +6 -0
- package/dist/tools/notebook-edit.js +127 -0
- package/dist/tools/worktree.d.ts +10 -0
- package/dist/tools/worktree.js +363 -0
- 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";
|