@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,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code `--output-format stream-json` → CodeShell `StreamEvent`.
|
|
3
|
+
*
|
|
4
|
+
* Shapes measured against real `claude 2.1.220`, not inferred. The stream is a
|
|
5
|
+
* mix of two vocabularies:
|
|
6
|
+
*
|
|
7
|
+
* - CodeShell-ish envelopes: `system` (with `subtype: "init"`), `assistant`,
|
|
8
|
+
* `user` (carrying tool results), `result`, `rate_limit_event`.
|
|
9
|
+
* - Raw Anthropic streaming events under `type: "stream_event"` →
|
|
10
|
+
* `event.type: message_start | content_block_start | content_block_delta | …`.
|
|
11
|
+
*
|
|
12
|
+
* Tool lifecycle comes from the RAW events, because only they carry the tool id
|
|
13
|
+
* at `content_block.id`. Text likewise: `assistant` repeats the whole message, so
|
|
14
|
+
* translating both it and the deltas would double every character.
|
|
15
|
+
*
|
|
16
|
+
* §15.2 still applies: a CodeShell Host Tool already emits its own lifecycle
|
|
17
|
+
* through `ToolExecutor`, so a `mcp__codeshell_tools__*` tool_use must NOT get a
|
|
18
|
+
* second card here. Claude namespaces MCP tools with that prefix, which is what
|
|
19
|
+
* makes the distinction reliable.
|
|
20
|
+
*/
|
|
21
|
+
import type { StreamEvent } from "@cjhyy/code-shell-core/extension";
|
|
22
|
+
export interface ClaudeEventTranslatorOptions {
|
|
23
|
+
/** CodeShell business session id, for correlating logs. */
|
|
24
|
+
sessionId: string;
|
|
25
|
+
/** MCP server name whose tool cards this translator suppresses. */
|
|
26
|
+
codeshellServerName?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare class ClaudeEventTranslator {
|
|
29
|
+
private readonly options;
|
|
30
|
+
private readonly codeshellServer;
|
|
31
|
+
private turnNumber;
|
|
32
|
+
private sessionStarted;
|
|
33
|
+
private terminal;
|
|
34
|
+
/** Accumulates `input_json_delta` per tool block so args land as one object. */
|
|
35
|
+
private readonly toolInput;
|
|
36
|
+
constructor(options: ClaudeEventTranslatorOptions);
|
|
37
|
+
/** Claude session id, learned from the `system/init` line. */
|
|
38
|
+
runtimeSessionId?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Translate one NDJSON line. Zero events is a normal outcome (housekeeping,
|
|
41
|
+
* unknown type). Deliberately total: an unrecognised or malformed line must not
|
|
42
|
+
* take the session down.
|
|
43
|
+
*/
|
|
44
|
+
translate(line: unknown): StreamEvent[];
|
|
45
|
+
private onSystem;
|
|
46
|
+
private onStreamEvent;
|
|
47
|
+
/** The most recently opened tool block — deltas belong to it. */
|
|
48
|
+
private currentTool;
|
|
49
|
+
private flushToolArgs;
|
|
50
|
+
/** Tool results arrive as a `user` message carrying `tool_result` blocks. */
|
|
51
|
+
private onUser;
|
|
52
|
+
private onResult;
|
|
53
|
+
private isCodeshellHostTool;
|
|
54
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
function asRecord(value) {
|
|
2
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
3
|
+
? value
|
|
4
|
+
: undefined;
|
|
5
|
+
}
|
|
6
|
+
function str(value) {
|
|
7
|
+
return typeof value === "string" && value ? value : undefined;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Map Claude's terminal reason onto CodeShell's.
|
|
11
|
+
*
|
|
12
|
+
* `error_max_turns` is distinct from a model error: reporting the wrong one sends
|
|
13
|
+
* a reader looking for an outage that never happened.
|
|
14
|
+
*/
|
|
15
|
+
function terminalReasonFor(subtype, isError) {
|
|
16
|
+
if (subtype === "error_max_turns")
|
|
17
|
+
return "max_turns";
|
|
18
|
+
if (subtype === "error_during_execution" || isError)
|
|
19
|
+
return "model_error";
|
|
20
|
+
return "completed";
|
|
21
|
+
}
|
|
22
|
+
export class ClaudeEventTranslator {
|
|
23
|
+
options;
|
|
24
|
+
codeshellServer;
|
|
25
|
+
turnNumber = 0;
|
|
26
|
+
sessionStarted = false;
|
|
27
|
+
terminal = false;
|
|
28
|
+
/** Accumulates `input_json_delta` per tool block so args land as one object. */
|
|
29
|
+
toolInput = new Map();
|
|
30
|
+
constructor(options) {
|
|
31
|
+
this.options = options;
|
|
32
|
+
this.codeshellServer = options.codeshellServerName ?? "codeshell_tools";
|
|
33
|
+
}
|
|
34
|
+
/** Claude session id, learned from the `system/init` line. */
|
|
35
|
+
runtimeSessionId;
|
|
36
|
+
/**
|
|
37
|
+
* Translate one NDJSON line. Zero events is a normal outcome (housekeeping,
|
|
38
|
+
* unknown type). Deliberately total: an unrecognised or malformed line must not
|
|
39
|
+
* take the session down.
|
|
40
|
+
*/
|
|
41
|
+
translate(line) {
|
|
42
|
+
const message = asRecord(line);
|
|
43
|
+
if (!message)
|
|
44
|
+
return [];
|
|
45
|
+
switch (str(message.type)) {
|
|
46
|
+
case "system":
|
|
47
|
+
return this.onSystem(message);
|
|
48
|
+
case "stream_event":
|
|
49
|
+
return this.onStreamEvent(message);
|
|
50
|
+
case "user":
|
|
51
|
+
return this.onUser(message);
|
|
52
|
+
case "result":
|
|
53
|
+
return this.onResult(message);
|
|
54
|
+
default:
|
|
55
|
+
// `assistant` is deliberately ignored: it repeats the full message text,
|
|
56
|
+
// which the content_block deltas have already delivered. Translating both
|
|
57
|
+
// would double every character.
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
onSystem(message) {
|
|
62
|
+
if (str(message.subtype) !== "init")
|
|
63
|
+
return [];
|
|
64
|
+
const sessionId = str(message.session_id);
|
|
65
|
+
if (sessionId)
|
|
66
|
+
this.runtimeSessionId = sessionId;
|
|
67
|
+
if (this.sessionStarted)
|
|
68
|
+
return [];
|
|
69
|
+
this.sessionStarted = true;
|
|
70
|
+
// promptTokens is unknown at init; the field is required, and 0 is honest
|
|
71
|
+
// here rather than a guess.
|
|
72
|
+
return sessionId ? [{ type: "session_started", sessionId, promptTokens: 0 }] : [];
|
|
73
|
+
}
|
|
74
|
+
onStreamEvent(message) {
|
|
75
|
+
const event = asRecord(message.event);
|
|
76
|
+
const type = str(event?.type);
|
|
77
|
+
if (!event || !type)
|
|
78
|
+
return [];
|
|
79
|
+
if (type === "message_start") {
|
|
80
|
+
this.turnNumber += 1;
|
|
81
|
+
return [{ type: "stream_request_start", turnNumber: this.turnNumber }];
|
|
82
|
+
}
|
|
83
|
+
if (type === "content_block_start") {
|
|
84
|
+
const block = asRecord(event.content_block);
|
|
85
|
+
if (str(block?.type) !== "tool_use")
|
|
86
|
+
return [];
|
|
87
|
+
const id = str(block?.id);
|
|
88
|
+
const name = str(block?.name);
|
|
89
|
+
if (!id || !name)
|
|
90
|
+
return [];
|
|
91
|
+
// Track it even when suppressed, so its input deltas do not leak into the
|
|
92
|
+
// next block's arguments.
|
|
93
|
+
this.toolInput.set(id, { name, json: "" });
|
|
94
|
+
if (this.isCodeshellHostTool(name))
|
|
95
|
+
return [];
|
|
96
|
+
return [{ type: "tool_use_start", toolCall: { id, toolName: name, args: {} } }];
|
|
97
|
+
}
|
|
98
|
+
if (type === "content_block_delta") {
|
|
99
|
+
const delta = asRecord(event.delta);
|
|
100
|
+
const deltaType = str(delta?.type);
|
|
101
|
+
if (deltaType === "text_delta") {
|
|
102
|
+
const text = str(delta?.text);
|
|
103
|
+
return text ? [{ type: "text_delta", text }] : [];
|
|
104
|
+
}
|
|
105
|
+
if (deltaType === "input_json_delta") {
|
|
106
|
+
// Arguments stream as JSON fragments; buffer and emit once complete.
|
|
107
|
+
const partial = typeof delta?.partial_json === "string" ? delta.partial_json : "";
|
|
108
|
+
const pending = this.currentTool();
|
|
109
|
+
if (pending)
|
|
110
|
+
pending.json += partial;
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
return [];
|
|
114
|
+
}
|
|
115
|
+
if (type === "content_block_stop") {
|
|
116
|
+
return this.flushToolArgs();
|
|
117
|
+
}
|
|
118
|
+
return [];
|
|
119
|
+
}
|
|
120
|
+
/** The most recently opened tool block — deltas belong to it. */
|
|
121
|
+
currentTool() {
|
|
122
|
+
let last;
|
|
123
|
+
for (const value of this.toolInput.values())
|
|
124
|
+
last = value;
|
|
125
|
+
return last;
|
|
126
|
+
}
|
|
127
|
+
flushToolArgs() {
|
|
128
|
+
const entries = [...this.toolInput.entries()];
|
|
129
|
+
const latest = entries.at(-1);
|
|
130
|
+
if (!latest)
|
|
131
|
+
return [];
|
|
132
|
+
const [id, pending] = latest;
|
|
133
|
+
if (this.isCodeshellHostTool(pending.name))
|
|
134
|
+
return [];
|
|
135
|
+
if (!pending.json)
|
|
136
|
+
return [];
|
|
137
|
+
let args;
|
|
138
|
+
try {
|
|
139
|
+
const parsed = JSON.parse(pending.json);
|
|
140
|
+
args = asRecord(parsed) ?? {};
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
// A truncated fragment is not worth failing a turn over; the tool_use_start
|
|
144
|
+
// already told the UI which tool is running.
|
|
145
|
+
return [];
|
|
146
|
+
}
|
|
147
|
+
return [{ type: "tool_use_args_delta", toolCallId: id, args }];
|
|
148
|
+
}
|
|
149
|
+
/** Tool results arrive as a `user` message carrying `tool_result` blocks. */
|
|
150
|
+
onUser(message) {
|
|
151
|
+
const inner = asRecord(message.message);
|
|
152
|
+
const content = Array.isArray(inner?.content) ? inner.content : [];
|
|
153
|
+
const events = [];
|
|
154
|
+
for (const raw of content) {
|
|
155
|
+
const block = asRecord(raw);
|
|
156
|
+
if (str(block?.type) !== "tool_result")
|
|
157
|
+
continue;
|
|
158
|
+
const id = str(block?.tool_use_id);
|
|
159
|
+
if (!id)
|
|
160
|
+
continue;
|
|
161
|
+
const known = this.toolInput.get(id);
|
|
162
|
+
// Suppress the CodeShell side; ToolExecutor already reported this call.
|
|
163
|
+
if (known && this.isCodeshellHostTool(known.name))
|
|
164
|
+
continue;
|
|
165
|
+
const text = typeof block?.content === "string"
|
|
166
|
+
? block.content
|
|
167
|
+
: Array.isArray(block?.content)
|
|
168
|
+
? block.content
|
|
169
|
+
.map((part) => str(asRecord(part)?.text) ?? "")
|
|
170
|
+
.filter(Boolean)
|
|
171
|
+
.join("\n")
|
|
172
|
+
: undefined;
|
|
173
|
+
events.push({
|
|
174
|
+
type: "tool_result",
|
|
175
|
+
result: {
|
|
176
|
+
id,
|
|
177
|
+
toolName: known?.name ?? "unknown",
|
|
178
|
+
...(text !== undefined ? { result: text } : {}),
|
|
179
|
+
...(block?.is_error === true ? { isError: true } : {}),
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
return events;
|
|
184
|
+
}
|
|
185
|
+
onResult(message) {
|
|
186
|
+
// `result` is the single terminal line. Guard against a duplicate closing the
|
|
187
|
+
// turn twice — the same hazard Codex has with turn/completed.
|
|
188
|
+
if (this.terminal)
|
|
189
|
+
return [];
|
|
190
|
+
this.terminal = true;
|
|
191
|
+
return [
|
|
192
|
+
{
|
|
193
|
+
type: "turn_complete",
|
|
194
|
+
reason: terminalReasonFor(str(message.subtype), message.is_error === true),
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
isCodeshellHostTool(toolName) {
|
|
199
|
+
return toolName.startsWith(`mcp__${this.codeshellServer}__`);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { McpBridgeHandle } from "../shared/mcp-bridge.js";
|
|
2
|
+
/** Logical MCP server name; also the `mcp__<server>__<tool>` prefix. */
|
|
3
|
+
export declare const CLAUDE_MCP_SERVER_NAME = "codeshell_tools";
|
|
4
|
+
export interface ClaudeMcpConfigOptions {
|
|
5
|
+
bridge: McpBridgeHandle;
|
|
6
|
+
serverName?: string;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* The `--mcp-config` payload as an inline JSON string.
|
|
10
|
+
*
|
|
11
|
+
* Prefer {@link writeClaudeMcpConfigFile} for anything that spawns a real
|
|
12
|
+
* process: an inline config puts the bearer token in `argv`, where `ps auxww`
|
|
13
|
+
* exposes it to every local user (measured on macOS — argv is not restricted to
|
|
14
|
+
* the owning uid). The bridge's only authentication IS that token, so leaking it
|
|
15
|
+
* hands another local process full session authority.
|
|
16
|
+
*
|
|
17
|
+
* Kept exported because it is the right shape for tests and for callers that hold
|
|
18
|
+
* no token (a config pointing at an already-authenticated transport).
|
|
19
|
+
*/
|
|
20
|
+
export declare function buildClaudeMcpConfig(options: ClaudeMcpConfigOptions): string;
|
|
21
|
+
export interface ClaudeMcpConfigFile {
|
|
22
|
+
/** Path to pass to `--mcp-config`. */
|
|
23
|
+
path: string;
|
|
24
|
+
/** Remove the file and its directory. Call when the session ends. */
|
|
25
|
+
cleanup(): void;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Write the config to a mode-0600 temp file so the token never enters `argv`.
|
|
29
|
+
*
|
|
30
|
+
* `--mcp-config` documents "JSON files or strings", and a file works identically
|
|
31
|
+
* — verified by capturing the resulting handshake, which carries the same
|
|
32
|
+
* `Authorization` header. This is per-invocation and unlinked on cleanup, so it
|
|
33
|
+
* does NOT touch the user's own `~/.claude.json`; the goal was never "avoid all
|
|
34
|
+
* files", it was "avoid mutating persistent user config".
|
|
35
|
+
*/
|
|
36
|
+
export declare function writeClaudeMcpConfigFile(options: ClaudeMcpConfigOptions): ClaudeMcpConfigFile;
|
|
37
|
+
/**
|
|
38
|
+
* Fully-qualified tool names for `--allowed-tools`.
|
|
39
|
+
*
|
|
40
|
+
* **`--allowed-tools` is a GRANT, not a filter.** Measured against real
|
|
41
|
+
* `claude 2.1.220`: `--allowed-tools Write` lets the model write a file with no
|
|
42
|
+
* approval prompt, while omitting the flag blocks the same request; and naming
|
|
43
|
+
* only `Read` does not disable `Write`. It is purely additive — the CLI pairs it
|
|
44
|
+
* with `--disallowedTools`, which is the deny half.
|
|
45
|
+
*
|
|
46
|
+
* An earlier version of this comment claimed the flag "does NOT widen anything".
|
|
47
|
+
* That was inverted, and the inversion is the dangerous part: a reader who
|
|
48
|
+
* believes the flag cannot widen has no reason to check what goes into it.
|
|
49
|
+
*
|
|
50
|
+
* Safety therefore rests on ONE property, enforced below rather than assumed:
|
|
51
|
+
* every emitted name is `mcp__<server>__<tool>`, which routes to CodeShell's own
|
|
52
|
+
* `ToolExecutor` and exposure policy. A bare `Bash` or `Write` reaching this list
|
|
53
|
+
* would grant the untrusted runtime an unapproved built-in tool, and the exposure
|
|
54
|
+
* allowlist would not help — it governs the MCP surface, not Claude Code's own
|
|
55
|
+
* tools.
|
|
56
|
+
*
|
|
57
|
+
* Note what is NOT done here: auto-prefixing. Turning `"Bash"` into
|
|
58
|
+
* `mcp__codeshell_tools__Bash` would make every input "valid" and defeat the
|
|
59
|
+
* check — the first version of this function did exactly that. A CodeShell tool
|
|
60
|
+
* name is a plain identifier, so it must be a plain identifier, and anything that
|
|
61
|
+
* looks like a built-in-tool pattern (`Bash(git *)`, `*`) or is already qualified
|
|
62
|
+
* for a different server is a programming error.
|
|
63
|
+
*/
|
|
64
|
+
export declare function claudeAllowedToolNames(toolNames: Iterable<string>, serverName?: string): string[];
|
|
65
|
+
/**
|
|
66
|
+
* CLI args wiring a `claude -p` invocation to the bridge.
|
|
67
|
+
*
|
|
68
|
+
* Uses a 0600 file for the config (token out of `argv`) and passes
|
|
69
|
+
* `--strict-mcp-config` so the bridge is the session's ONLY tool channel.
|
|
70
|
+
* Without that flag Claude Code also loads every MCP server from the user's
|
|
71
|
+
* `~/.claude.json` and project config — servers the user approved for their own
|
|
72
|
+
* interactive use, not for an agent CodeShell spawns on their behalf, and which
|
|
73
|
+
* sit entirely outside CodeShell's exposure policy and `ToolExecutor`.
|
|
74
|
+
*
|
|
75
|
+
* The caller owns `cleanup()`; run it when the session ends.
|
|
76
|
+
*/
|
|
77
|
+
export declare function claudeBridgeArgs(options: {
|
|
78
|
+
bridge: McpBridgeHandle;
|
|
79
|
+
exposedToolNames: Iterable<string>;
|
|
80
|
+
serverName?: string;
|
|
81
|
+
}): {
|
|
82
|
+
args: string[];
|
|
83
|
+
cleanup(): void;
|
|
84
|
+
};
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wiring Claude Code to the loopback MCP bridge.
|
|
3
|
+
*
|
|
4
|
+
* The design (§10.2) assumed this would need `@anthropic-ai/claude-agent-sdk` and
|
|
5
|
+
* an in-process MCP server — a second transport alongside Codex's HTTP one.
|
|
6
|
+
* It does not. `claude --mcp-config` accepts an HTTP MCP server with an
|
|
7
|
+
* `Authorization` header, exactly as Codex does, so both runtimes share one bridge
|
|
8
|
+
* and CodeShell takes on no new dependency. Verified against real `claude 2.1.220`
|
|
9
|
+
* (`docs/todo/evidence/e2e-claude-product-bridge.mjs`).
|
|
10
|
+
*
|
|
11
|
+
* Two behaviours worth knowing, both observed:
|
|
12
|
+
*
|
|
13
|
+
* - Claude Code sends **no per-call thread identity**. Its bridge must therefore
|
|
14
|
+
* be pinned to one session (`singleSessionThreadId`), making the port the
|
|
15
|
+
* attribution. That is a real constraint, not a shortcut — see
|
|
16
|
+
* `McpBridgeOptions.singleSessionThreadId`.
|
|
17
|
+
* - It opens with a `GET` (probing for an SSE stream) which the bridge answers
|
|
18
|
+
* 405. Harmless: it falls back to POST immediately.
|
|
19
|
+
*/
|
|
20
|
+
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { tmpdir } from "node:os";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
/** Logical MCP server name; also the `mcp__<server>__<tool>` prefix. */
|
|
24
|
+
export const CLAUDE_MCP_SERVER_NAME = "codeshell_tools";
|
|
25
|
+
/** `mcp__<server>__<tool>` — the only shape that may reach `--allowed-tools`. */
|
|
26
|
+
const MCP_TOOL_NAME = /^mcp__[a-z0-9_]+__[A-Za-z0-9_]+$/;
|
|
27
|
+
/**
|
|
28
|
+
* Claude Code's own built-in tools. They share the plain-identifier shape with
|
|
29
|
+
* CodeShell tool names, so shape validation alone cannot separate them — and
|
|
30
|
+
* pre-approving one of these grants the untrusted runtime an unapproved built-in.
|
|
31
|
+
* Over-inclusive on purpose: a false positive is a loud error at a call site, a
|
|
32
|
+
* false negative is a silent capability grant.
|
|
33
|
+
*/
|
|
34
|
+
const CLAUDE_BUILTIN_TOOL_NAMES = new Set([
|
|
35
|
+
"Agent",
|
|
36
|
+
"Bash",
|
|
37
|
+
"BashOutput",
|
|
38
|
+
"Edit",
|
|
39
|
+
"ExitPlanMode",
|
|
40
|
+
"Glob",
|
|
41
|
+
"Grep",
|
|
42
|
+
"KillShell",
|
|
43
|
+
"NotebookEdit",
|
|
44
|
+
"Read",
|
|
45
|
+
"Task",
|
|
46
|
+
"TodoWrite",
|
|
47
|
+
"WebFetch",
|
|
48
|
+
"WebSearch",
|
|
49
|
+
"Write",
|
|
50
|
+
]);
|
|
51
|
+
/**
|
|
52
|
+
* The `--mcp-config` payload as an inline JSON string.
|
|
53
|
+
*
|
|
54
|
+
* Prefer {@link writeClaudeMcpConfigFile} for anything that spawns a real
|
|
55
|
+
* process: an inline config puts the bearer token in `argv`, where `ps auxww`
|
|
56
|
+
* exposes it to every local user (measured on macOS — argv is not restricted to
|
|
57
|
+
* the owning uid). The bridge's only authentication IS that token, so leaking it
|
|
58
|
+
* hands another local process full session authority.
|
|
59
|
+
*
|
|
60
|
+
* Kept exported because it is the right shape for tests and for callers that hold
|
|
61
|
+
* no token (a config pointing at an already-authenticated transport).
|
|
62
|
+
*/
|
|
63
|
+
export function buildClaudeMcpConfig(options) {
|
|
64
|
+
const serverName = options.serverName ?? CLAUDE_MCP_SERVER_NAME;
|
|
65
|
+
return JSON.stringify({
|
|
66
|
+
mcpServers: {
|
|
67
|
+
[serverName]: {
|
|
68
|
+
type: "http",
|
|
69
|
+
url: options.bridge.url,
|
|
70
|
+
headers: { Authorization: `Bearer ${options.bridge.token}` },
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Write the config to a mode-0600 temp file so the token never enters `argv`.
|
|
77
|
+
*
|
|
78
|
+
* `--mcp-config` documents "JSON files or strings", and a file works identically
|
|
79
|
+
* — verified by capturing the resulting handshake, which carries the same
|
|
80
|
+
* `Authorization` header. This is per-invocation and unlinked on cleanup, so it
|
|
81
|
+
* does NOT touch the user's own `~/.claude.json`; the goal was never "avoid all
|
|
82
|
+
* files", it was "avoid mutating persistent user config".
|
|
83
|
+
*/
|
|
84
|
+
export function writeClaudeMcpConfigFile(options) {
|
|
85
|
+
const dir = mkdtempSync(join(tmpdir(), "codeshell-mcp-"));
|
|
86
|
+
const path = join(dir, "mcp-config.json");
|
|
87
|
+
writeFileSync(path, buildClaudeMcpConfig(options), { mode: 0o600 });
|
|
88
|
+
// Set explicitly as well: writeFileSync's mode is subject to umask.
|
|
89
|
+
chmodSync(path, 0o600);
|
|
90
|
+
return {
|
|
91
|
+
path,
|
|
92
|
+
cleanup: () => rmSync(dir, { recursive: true, force: true }),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Fully-qualified tool names for `--allowed-tools`.
|
|
97
|
+
*
|
|
98
|
+
* **`--allowed-tools` is a GRANT, not a filter.** Measured against real
|
|
99
|
+
* `claude 2.1.220`: `--allowed-tools Write` lets the model write a file with no
|
|
100
|
+
* approval prompt, while omitting the flag blocks the same request; and naming
|
|
101
|
+
* only `Read` does not disable `Write`. It is purely additive — the CLI pairs it
|
|
102
|
+
* with `--disallowedTools`, which is the deny half.
|
|
103
|
+
*
|
|
104
|
+
* An earlier version of this comment claimed the flag "does NOT widen anything".
|
|
105
|
+
* That was inverted, and the inversion is the dangerous part: a reader who
|
|
106
|
+
* believes the flag cannot widen has no reason to check what goes into it.
|
|
107
|
+
*
|
|
108
|
+
* Safety therefore rests on ONE property, enforced below rather than assumed:
|
|
109
|
+
* every emitted name is `mcp__<server>__<tool>`, which routes to CodeShell's own
|
|
110
|
+
* `ToolExecutor` and exposure policy. A bare `Bash` or `Write` reaching this list
|
|
111
|
+
* would grant the untrusted runtime an unapproved built-in tool, and the exposure
|
|
112
|
+
* allowlist would not help — it governs the MCP surface, not Claude Code's own
|
|
113
|
+
* tools.
|
|
114
|
+
*
|
|
115
|
+
* Note what is NOT done here: auto-prefixing. Turning `"Bash"` into
|
|
116
|
+
* `mcp__codeshell_tools__Bash` would make every input "valid" and defeat the
|
|
117
|
+
* check — the first version of this function did exactly that. A CodeShell tool
|
|
118
|
+
* name is a plain identifier, so it must be a plain identifier, and anything that
|
|
119
|
+
* looks like a built-in-tool pattern (`Bash(git *)`, `*`) or is already qualified
|
|
120
|
+
* for a different server is a programming error.
|
|
121
|
+
*/
|
|
122
|
+
export function claudeAllowedToolNames(toolNames, serverName = CLAUDE_MCP_SERVER_NAME) {
|
|
123
|
+
const reject = (name, why) => {
|
|
124
|
+
throw new Error(`Refusing to pre-approve '${name}': ${why}. --allowed-tools is a GRANT, and ` +
|
|
125
|
+
`only mcp__${serverName}__<tool> names route through CodeShell's ToolExecutor.`);
|
|
126
|
+
};
|
|
127
|
+
return [...toolNames].map((name) => {
|
|
128
|
+
// A CodeShell tool name, e.g. "Panel". Nothing else is accepted.
|
|
129
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*$/.test(name)) {
|
|
130
|
+
return reject(name, "not a plain CodeShell tool name");
|
|
131
|
+
}
|
|
132
|
+
// An already-qualified name would let a caller pre-approve a tool on a
|
|
133
|
+
// DIFFERENT MCP server — outside CodeShell's exposure policy entirely. The
|
|
134
|
+
// qualifying is this function's job, not the caller's. (Checked separately
|
|
135
|
+
// from the shape test above: `mcp__other__Thing` is a valid identifier.)
|
|
136
|
+
if (name.startsWith("mcp__")) {
|
|
137
|
+
return reject(name, "already qualified — pass a plain CodeShell tool name");
|
|
138
|
+
}
|
|
139
|
+
// Claude Code's own built-ins share the identifier shape, so shape alone is
|
|
140
|
+
// not enough — these must never be pre-approved through this path.
|
|
141
|
+
if (CLAUDE_BUILTIN_TOOL_NAMES.has(name)) {
|
|
142
|
+
return reject(name, "this is a Claude Code built-in tool, not a CodeShell tool");
|
|
143
|
+
}
|
|
144
|
+
const qualified = `mcp__${serverName}__${name}`;
|
|
145
|
+
if (!MCP_TOOL_NAME.test(qualified))
|
|
146
|
+
return reject(name, "does not qualify as an MCP tool name");
|
|
147
|
+
return qualified;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* CLI args wiring a `claude -p` invocation to the bridge.
|
|
152
|
+
*
|
|
153
|
+
* Uses a 0600 file for the config (token out of `argv`) and passes
|
|
154
|
+
* `--strict-mcp-config` so the bridge is the session's ONLY tool channel.
|
|
155
|
+
* Without that flag Claude Code also loads every MCP server from the user's
|
|
156
|
+
* `~/.claude.json` and project config — servers the user approved for their own
|
|
157
|
+
* interactive use, not for an agent CodeShell spawns on their behalf, and which
|
|
158
|
+
* sit entirely outside CodeShell's exposure policy and `ToolExecutor`.
|
|
159
|
+
*
|
|
160
|
+
* The caller owns `cleanup()`; run it when the session ends.
|
|
161
|
+
*/
|
|
162
|
+
export function claudeBridgeArgs(options) {
|
|
163
|
+
const serverName = options.serverName ?? CLAUDE_MCP_SERVER_NAME;
|
|
164
|
+
const config = writeClaudeMcpConfigFile({ bridge: options.bridge, serverName });
|
|
165
|
+
return {
|
|
166
|
+
args: [
|
|
167
|
+
"--mcp-config",
|
|
168
|
+
config.path,
|
|
169
|
+
"--strict-mcp-config",
|
|
170
|
+
"--allowed-tools",
|
|
171
|
+
claudeAllowedToolNames(options.exposedToolNames, serverName).join(","),
|
|
172
|
+
],
|
|
173
|
+
cleanup: config.cleanup,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { StreamEvent } from "@cjhyy/code-shell-core/extension";
|
|
2
|
+
import type { McpBridgeHandle } from "../shared/mcp-bridge.js";
|
|
3
|
+
export interface ClaudeRuntimeOptions {
|
|
4
|
+
cwd: string;
|
|
5
|
+
/** CodeShell business session id — the authorization subject. */
|
|
6
|
+
businessSessionId: string;
|
|
7
|
+
bridge: McpBridgeHandle;
|
|
8
|
+
/** CodeShell tool names the bridge exposes, for `--allowed-tools`. */
|
|
9
|
+
exposedToolNames: readonly string[];
|
|
10
|
+
/** Executable; defaults to `claude` on PATH. */
|
|
11
|
+
command?: string;
|
|
12
|
+
/** Extra args, inserted before the runtime's own. */
|
|
13
|
+
extraArgs?: readonly string[];
|
|
14
|
+
model?: string;
|
|
15
|
+
serverName?: string;
|
|
16
|
+
log?: (event: string, data: Record<string, unknown>) => void;
|
|
17
|
+
}
|
|
18
|
+
export interface ClaudeRuntimeHooks {
|
|
19
|
+
onEvent?: (event: StreamEvent) => void;
|
|
20
|
+
}
|
|
21
|
+
export interface ClaudeTurnHandle {
|
|
22
|
+
readonly done: Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
export declare class ClaudeCodeRuntime {
|
|
25
|
+
private readonly options;
|
|
26
|
+
private readonly hooks;
|
|
27
|
+
readonly kind: "claude-code";
|
|
28
|
+
private readonly log;
|
|
29
|
+
private translator;
|
|
30
|
+
private child?;
|
|
31
|
+
private claudeSessionId?;
|
|
32
|
+
private closed;
|
|
33
|
+
constructor(options: ClaudeRuntimeOptions, hooks?: ClaudeRuntimeHooks);
|
|
34
|
+
/** Claude session id, once the first turn has reported it. Resume key only. */
|
|
35
|
+
get runtimeSessionId(): string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* Run one turn. Resolves when the process exits, which is also when the turn is
|
|
38
|
+
* terminal — `claude -p` is one process per turn.
|
|
39
|
+
*
|
|
40
|
+
* The prompt goes on **stdin**, not argv: `--mcp-config` is variadic, so a
|
|
41
|
+
* positional prompt after it is swallowed as another config value (measured),
|
|
42
|
+
* and a prompt on the command line would also be visible in `ps`.
|
|
43
|
+
*/
|
|
44
|
+
send(text: string): Promise<ClaudeTurnHandle>;
|
|
45
|
+
private onLine;
|
|
46
|
+
/**
|
|
47
|
+
* Interrupt the active turn.
|
|
48
|
+
*
|
|
49
|
+
* `claude -p` has no interrupt RPC, so this is a signal. As with Codex, it
|
|
50
|
+
* cannot retract side effects already dispatched — it only stops further steps.
|
|
51
|
+
*/
|
|
52
|
+
interrupt(): Promise<void>;
|
|
53
|
+
close(): Promise<void>;
|
|
54
|
+
/** Start a fresh conversation on the next `send()`, discarding resume state. */
|
|
55
|
+
resetConversation(): void;
|
|
56
|
+
}
|