@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,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
+ }