@heyditto/cli 2.30.0 → 2.32.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 CHANGED
@@ -499,6 +499,7 @@ Options shared by both commands:
499
499
  | `--session <id>` | reuse a Ditto session id so traces land in an existing thread |
500
500
  | `--resume [id]` / `-c, --continue` | resume a local session (`heyditto sessions`) / the agent's most recent conversation. Claude also takes a Claude session id or title here, including sessions started in Claude Desktop or plain `claude`, and runs in the directory that session ran in; with no value, a terminal gets a searchable picker. Claude's own `-r` / `--resume` / `-c` after `--` do the same. |
501
501
  | `--yolo` / `--yellow` / `--plan` | bypass permissions / auto-accept edits / plan mode (Claude only) |
502
+ | `--mcp` / `--no-mcp` | attach the endpoint's tools to Claude Code as its own MCP servers, or keep them on the router (default: the endpoint's `claude_tool_surface`, else the router) |
502
503
  | `-p, --prompt <text>` | headless run: `claude -p` or `codex exec` |
503
504
  | `-m, --model <id>` | model to request (default: the agent's own model id passes through and the endpoint routes it) |
504
505
  | `-w, --worktree [name]` | run inside `<repo>/.worktrees/<name>` on a branch of that name; `.worktrees/` is added to `.gitignore` |
@@ -519,6 +520,25 @@ How the wiring works:
519
520
  a bare `mcp__ditto` denies them all) in your user, project or local
520
521
  `settings.json` are forwarded as `X-Ditto-Tool-Deny`, and the router refuses
521
522
  those tools for the session. The header can only add denials.
523
+ - **Claude Code with `--mcp`** attaches the endpoint's tools as its own MCP
524
+ servers instead: `ditto` for Ditto's built-ins, plus one server per
525
+ connected app or MCP server (`ditto-github`, `ditto-jira`, …). Claude Code
526
+ discovers them with its own ToolSearch and calls them natively. Every call
527
+ still runs on Ditto (`/v1/mcp/<server>`) with the credentials Ditto holds and
528
+ behind the endpoint's approval gate and deny rules, and the router is told
529
+ not to offer the same tools again. The session key reaches the MCP headers
530
+ as `${DITTO_ROUTER_MCP_KEY}`, never argv or disk. Who asks before a gated
531
+ call is the endpoint's choice:
532
+
533
+ | Endpoint | What happens to a gated call |
534
+ | --- | --- |
535
+ | default | Ditto holds it for approval; Claude Code does not ask a second time |
536
+ | `client_approvals = true` | Claude Code asks; `--yolo` runs it without asking. Deny rules still refuse |
537
+
538
+ `client_approvals` hands approval to whoever runs the session, so only an
539
+ organization's managers can turn it on for an org endpoint. Code Mode and
540
+ the router's own tools stay the default; set `claude_tool_surface = "mcp"`
541
+ in an endpoint profile to make `--mcp` the default there.
522
542
  - **Codex** speaks the Responses API only, so the endpoint is injected as a
523
543
  `ditto` model provider via `-c` overrides (nothing is written to
524
544
  `~/.codex/config.toml`) with the key in `DITTO_INFERENCE_API_KEY`.
@@ -1039,7 +1059,7 @@ Settings: `owner`, `claude_prompt` / `codex_prompt` (`file`, `mode`,
1039
1059
  `precompact_at_tokens`, `max_tool_rounds`, `memory_depth`, `recall`, `record`,
1040
1060
  `record_trace`, `record_attachments`, `trace_retention_days`, `batch`,
1041
1061
  `batch_max_requests`, `tools`, `tool_approval_mode`, `auto_approve_tools`,
1042
- `tool_deny`, `required_connections`, `kind_routes`, `model_routes`,
1062
+ `tool_deny`, `claude_native_tool_blocks`, `required_connections`, `kind_routes`, `model_routes`,
1043
1063
  `subagent_routes`, `aliases`.
1044
1064
 
1045
1065
  `tool_deny` lists router tools the endpoint never runs, whatever a client asks:
@@ -1048,6 +1068,12 @@ tool names, globs such as `"gmail__send_*"`, or a class (`"class:gated"`,
1048
1068
  refused. On an organization endpoint, changing it needs endpoint policy
1049
1069
  authority.
1050
1070
 
1071
+ `claude_native_tool_blocks = true` writes each router tool call and its result
1072
+ into Claude Code's own transcript (`server_tool_use` / `mcp_tool_result`), so
1073
+ the router rebuilds earlier turns from the transcript instead of its memory, and
1074
+ a deploy no longer loses them. Claude Code does not draw these blocks in its live
1075
+ view; they appear in the session file and in tools that read it.
1076
+
1051
1077
  Layers: workspace `.ditto/` (a parent folder of many repos) < repository `.ditto/` <
1052
1078
  ignored `.ditto/local.toml` < flags. Secrets are never literals: use `secret://`
1053
1079
  references. Paths are repository-relative and cannot escape it.
@@ -11,6 +11,8 @@ import path from "node:path";
11
11
  export const TOOL_DENY_HEADER = "X-Ditto-Tool-Deny";
12
12
  const NAMESPACE = "mcp__ditto";
13
13
  const VALID = /^[A-Za-z0-9_.*-]+$/;
14
+ /** Native MCP mode's per-source servers (claude-mcp.ts): mcp__ditto-<source>[__<tool>]. */
15
+ const SOURCE_SERVER = /^mcp__ditto-([a-z0-9][a-z0-9_-]*?)(?:__(.+))?$/i;
14
16
  /**
15
17
  * The deny rules for router tools, from every settings file Claude Code may
16
18
  * read for this directory: user, then project and local settings in the
@@ -45,6 +47,13 @@ export function claudeDittoDenyRules(cwd, env) {
45
47
  else if (rule.toLowerCase().startsWith(`${NAMESPACE}__`) && VALID.test(rule)) {
46
48
  rules.push(rule.slice(NAMESPACE.length + 2));
47
49
  }
50
+ else if (VALID.test(rule)) {
51
+ // The router names a source's tools <source>__<tool>, so a rule on
52
+ // the server mcp__ditto-github is the router rule github__*.
53
+ const source = SOURCE_SERVER.exec(rule);
54
+ if (source)
55
+ rules.push(`${source[1]}__${source[2] ?? "*"}`);
56
+ }
48
57
  }
49
58
  }
50
59
  return [...new Set(rules)];
@@ -1 +1 @@
1
- {"version":3,"file":"claude-deny.js","sourceRoot":"","sources":["../../src/agents/claude-deny.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,mBAAmB,CAAC;AAEpD,MAAM,SAAS,GAAG,YAAY,CAAC;AAC/B,MAAM,KAAK,GAAG,oBAAoB,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,UAAU,oBAAoB,CAAC,GAAW,EAAE,GAAsB;IACtE,MAAM,IAAI,GAAG,GAAG,CAAC,iBAAiB,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,CAAC,CAAC;IACjF,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC,CAAC;IACjD,KAAK,MAAM,GAAG,IAAI,WAAW,CAAC,GAAG,CAAC;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,eAAe,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,qBAAqB,CAAC,CAAC,CAAC;IAC7I,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,IAAa,CAAC;QAClB,IAAI,CAAC;YACH,IAAI,GAAI,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,CAA0C,CAAC,WAAW,EAAE,IAAI,CAAC;QAC5G,CAAC;QAAC,MAAM,CAAC;YACP,SAAS;QACX,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC;YAAE,SAAS;QACnC,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,IAAI,OAAO,KAAK,KAAK,QAAQ;gBAAE,SAAS;YACxC,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;YAC1B,IAAI,IAAI,CAAC,WAAW,EAAE,KAAK,SAAS,EAAE,CAAC;gBACrC,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;YAClB,CAAC;iBAAM,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC,UAAU,CAAC,GAAG,SAAS,IAAI,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC/E,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;YAC/C,CAAC;QACH,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC;AAC7B,CAAC;AAED,+FAA+F;AAC/F,SAAS,WAAW,CAAC,GAAW;IAC9B,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,KAAK,IAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAI,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;QAC5D,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACf,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QACpD,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,GAAG;YAAE,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"claude-deny.js","sourceRoot":"","sources":["../../src/agents/claude-deny.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Native MCP mode for Claude Code (DITTO-1497).
3
+ *
4
+ * The router normally runs an endpoint's tools itself: injected into each
5
+ * request, or behind Code Mode's search_tools/run_code sandbox. In this mode
6
+ * the endpoint's tools arrive as Claude Code's own MCP servers instead, one
7
+ * per source ("ditto" for Ditto's built-ins, "ditto-github", "ditto-linear",
8
+ * …), so Claude Code discovers them with its own ToolSearch and calls them
9
+ * natively. Every call still executes on Ditto (`/v1/mcp/{server}`) with the
10
+ * credentials Ditto holds, behind the endpoint's approval gate and deny rules.
11
+ *
12
+ * Who asks before a gated call is the endpoint's choice (`clientApprovals`):
13
+ *
14
+ * - off (default): Ditto holds gated calls itself, so every Ditto server is
15
+ * allowed here and nobody is asked twice;
16
+ * - on: Ditto holds nothing on this surface. Calls Ditto would have held get
17
+ * an `ask` rule, and `--yolo` (bypass) runs them without one.
18
+ */
19
+ /** Tells the router this session attached the tools itself, so it injects none. */
20
+ export declare const TOOL_SURFACE_HEADER = "X-Ditto-Tool-Surface";
21
+ export declare const TOOL_SURFACE_MCP = "mcp";
22
+ /**
23
+ * The endpoint key reaches Claude Code's MCP headers through this variable,
24
+ * which Claude Code expands in `--mcp-config` (`${VAR}`), so the key never
25
+ * lands in a file or in argv.
26
+ */
27
+ export declare const MCP_KEY_ENV = "DITTO_ROUTER_MCP_KEY";
28
+ export interface McpCatalogTool {
29
+ name: string;
30
+ tier: string;
31
+ /** Ditto's policy holds this call for a person. */
32
+ approval: boolean;
33
+ }
34
+ export interface McpCatalogServer {
35
+ name: string;
36
+ url: string;
37
+ tools: McpCatalogTool[];
38
+ }
39
+ /** GET /v1/mcp. */
40
+ export interface McpCatalog {
41
+ endpoint: string;
42
+ approvalMode: string;
43
+ clientApprovals: boolean;
44
+ servers: McpCatalogServer[];
45
+ }
46
+ /** What a Claude launch needs to attach the catalog. */
47
+ export interface ClaudeMcpAttachment {
48
+ /** The `--mcp-config` JSON. */
49
+ config: string;
50
+ /** permissions.allow / permissions.ask for `--settings`. */
51
+ allow: string[];
52
+ ask: string[];
53
+ servers: number;
54
+ tools: number;
55
+ clientApprovals: boolean;
56
+ }
57
+ /** Claude Code server name for a catalog server: the built-ins keep "ditto", the rest are "ditto-<source>". */
58
+ export declare function claudeServerName(server: string): string;
59
+ /** Reads the endpoint's MCP catalog with the session's own key. */
60
+ export declare function fetchMcpCatalog(baseUrl: string, apiKey: string, sessionId: string, fetchImpl?: typeof fetch, timeoutMs?: number): Promise<McpCatalog>;
61
+ /**
62
+ * The Claude Code configuration for a catalog: one http MCP server per
63
+ * source, and the permission rules that put the endpoint's approval policy
64
+ * into Claude Code's own prompts.
65
+ */
66
+ export declare function claudeMcpAttachment(catalog: McpCatalog, sessionId: string, opts?: {
67
+ yolo?: boolean;
68
+ }): ClaudeMcpAttachment;
69
+ type Json = Record<string, unknown>;
70
+ /** Objects merge key by key, arrays concatenate without duplicates, anything else: b wins. */
71
+ export declare function mergeSettings(a: Json, b: Json): Json;
72
+ export interface SettingsIO {
73
+ /** Claude's launch directory: a relative settings path means a file there. */
74
+ cwd?: string;
75
+ read?: (file: string) => string;
76
+ /** Writes merged settings somewhere only this user can read; returns the path. */
77
+ writePrivate?: (json: string) => string;
78
+ }
79
+ /**
80
+ * Removes settings copies whose CLI is gone (killed by a signal before its exit
81
+ * cleanup ran). Liveness, not age: a session can run for days, and its Claude
82
+ * keeps reading its settings for as long as it runs.
83
+ */
84
+ export declare function sweepStaleSettings(tmp: string, isAlive?: (pid: number) => boolean): void;
85
+ /**
86
+ * A private copy of merged settings for the life of this process. A user's
87
+ * settings file can hold secrets, so its contents never go into Claude's
88
+ * argv (visible in process listings); the copy is a 0600 file in a 0700
89
+ * directory. Copies are named by content, so a long headless session that
90
+ * re-plans every turn reuses one file per distinct value instead of piling
91
+ * them up. The directory goes when the CLI exits; one left by a signal is
92
+ * swept by a later launch once its process is gone. No signal handlers are
93
+ * installed: the launcher owns Ctrl+C, and re-raising a signal here would cut
94
+ * its shutdown short.
95
+ */
96
+ export declare function writePrivateSettings(json: string, tmp?: string): string;
97
+ /**
98
+ * Adds settings to a Claude argv. Claude Code reads a single `--settings`, so
99
+ * the one already in the argv absorbs the new keys rather than a second flag
100
+ * silently replacing either. Inline JSON is merged in place. A settings file is
101
+ * read from Claude's launch directory and merged into a private copy whose path
102
+ * replaces it. A file that cannot be read stops the launch, since dropping it
103
+ * would lose the user's settings and dropping ours their MCP permissions.
104
+ */
105
+ export declare function withClaudeSettings(args: string[], settings: Json, io?: SettingsIO): string[];
106
+ /** The permission settings an attachment contributes. */
107
+ export declare function attachmentSettings(mcp: ClaudeMcpAttachment): Json;
108
+ export {};
@@ -0,0 +1,247 @@
1
+ import { createHash } from "node:crypto";
2
+ import { chmodSync, existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ /**
6
+ * Native MCP mode for Claude Code (DITTO-1497).
7
+ *
8
+ * The router normally runs an endpoint's tools itself: injected into each
9
+ * request, or behind Code Mode's search_tools/run_code sandbox. In this mode
10
+ * the endpoint's tools arrive as Claude Code's own MCP servers instead, one
11
+ * per source ("ditto" for Ditto's built-ins, "ditto-github", "ditto-linear",
12
+ * …), so Claude Code discovers them with its own ToolSearch and calls them
13
+ * natively. Every call still executes on Ditto (`/v1/mcp/{server}`) with the
14
+ * credentials Ditto holds, behind the endpoint's approval gate and deny rules.
15
+ *
16
+ * Who asks before a gated call is the endpoint's choice (`clientApprovals`):
17
+ *
18
+ * - off (default): Ditto holds gated calls itself, so every Ditto server is
19
+ * allowed here and nobody is asked twice;
20
+ * - on: Ditto holds nothing on this surface. Calls Ditto would have held get
21
+ * an `ask` rule, and `--yolo` (bypass) runs them without one.
22
+ */
23
+ /** Tells the router this session attached the tools itself, so it injects none. */
24
+ export const TOOL_SURFACE_HEADER = "X-Ditto-Tool-Surface";
25
+ export const TOOL_SURFACE_MCP = "mcp";
26
+ /**
27
+ * The endpoint key reaches Claude Code's MCP headers through this variable,
28
+ * which Claude Code expands in `--mcp-config` (`${VAR}`), so the key never
29
+ * lands in a file or in argv.
30
+ */
31
+ export const MCP_KEY_ENV = "DITTO_ROUTER_MCP_KEY";
32
+ /** Claude Code server name for a catalog server: the built-ins keep "ditto", the rest are "ditto-<source>". */
33
+ export function claudeServerName(server) {
34
+ return server === "ditto" ? "ditto" : `ditto-${server}`;
35
+ }
36
+ /** A stalled catalog read must not hold up Claude's launch; the caller falls back to the router. */
37
+ const CATALOG_TIMEOUT_MS = 15_000;
38
+ /** Reads the endpoint's MCP catalog with the session's own key. */
39
+ export async function fetchMcpCatalog(baseUrl, apiKey, sessionId, fetchImpl = fetch, timeoutMs = CATALOG_TIMEOUT_MS) {
40
+ const url = `${baseUrl.replace(/\/+$/, "")}/mcp`;
41
+ const res = await fetchImpl(url, {
42
+ headers: { Authorization: `Bearer ${apiKey}`, "X-Ditto-Session-Id": sessionId, Accept: "application/json" },
43
+ signal: AbortSignal.timeout(timeoutMs),
44
+ });
45
+ if (!res.ok) {
46
+ let detail = "";
47
+ try {
48
+ const body = (await res.json());
49
+ detail = body.error?.message ?? "";
50
+ }
51
+ catch {
52
+ // A body that is not JSON adds nothing the status does not say.
53
+ }
54
+ throw new Error(`GET ${url} → ${res.status}${detail ? `: ${detail}` : ""}`);
55
+ }
56
+ const catalog = (await res.json());
57
+ if (!Array.isArray(catalog.servers))
58
+ throw new Error(`GET ${url}: not an MCP catalog`);
59
+ return catalog;
60
+ }
61
+ /**
62
+ * The Claude Code configuration for a catalog: one http MCP server per
63
+ * source, and the permission rules that put the endpoint's approval policy
64
+ * into Claude Code's own prompts.
65
+ */
66
+ export function claudeMcpAttachment(catalog, sessionId, opts = {}) {
67
+ const mcpServers = {};
68
+ const allow = [];
69
+ const ask = [];
70
+ let tools = 0;
71
+ for (const server of catalog.servers) {
72
+ if (server.tools.length === 0)
73
+ continue;
74
+ const name = claudeServerName(server.name);
75
+ mcpServers[name] = {
76
+ type: "http",
77
+ url: server.url,
78
+ headers: { Authorization: `Bearer \${${MCP_KEY_ENV}}`, "X-Ditto-Session-Id": sessionId },
79
+ };
80
+ tools += server.tools.length;
81
+ if (!catalog.clientApprovals) {
82
+ // Ditto's gate is the authority here; a Claude prompt on top would ask twice.
83
+ allow.push(`mcp__${name}`);
84
+ continue;
85
+ }
86
+ for (const tool of server.tools) {
87
+ const rule = `mcp__${name}__${tool.name}`;
88
+ if (!tool.approval)
89
+ allow.push(rule);
90
+ // Under bypass the user chose not to be asked; an ask rule would still prompt.
91
+ else if (!opts.yolo)
92
+ ask.push(rule);
93
+ }
94
+ }
95
+ return {
96
+ config: JSON.stringify({ mcpServers }),
97
+ allow,
98
+ ask,
99
+ servers: Object.keys(mcpServers).length,
100
+ tools,
101
+ clientApprovals: catalog.clientApprovals,
102
+ };
103
+ }
104
+ function isObject(v) {
105
+ return typeof v === "object" && v !== null && !Array.isArray(v);
106
+ }
107
+ /** Objects merge key by key, arrays concatenate without duplicates, anything else: b wins. */
108
+ export function mergeSettings(a, b) {
109
+ const out = { ...a };
110
+ for (const [k, v] of Object.entries(b)) {
111
+ const cur = Object.prototype.hasOwnProperty.call(out, k) ? out[k] : undefined;
112
+ let value = v;
113
+ if (isObject(cur) && isObject(v))
114
+ value = mergeSettings(cur, v);
115
+ else if (Array.isArray(cur) && Array.isArray(v))
116
+ value = [...new Set([...cur, ...v])];
117
+ // defineProperty, so a "__proto__" key stays data instead of hitting the setter.
118
+ Object.defineProperty(out, k, { value, enumerable: true, configurable: true, writable: true });
119
+ }
120
+ return out;
121
+ }
122
+ const PRIVATE_SETTINGS_PREFIX = "ditto-claude-settings-";
123
+ /** One private directory per temp root for the life of this process. */
124
+ const privateSettingsDirs = new Map();
125
+ /** The pid a settings directory belongs to: ditto-claude-settings-<pid>-<random>. */
126
+ function ownerPid(name) {
127
+ const m = /^ditto-claude-settings-(\d+)-/.exec(name);
128
+ return m ? Number(m[1]) : undefined;
129
+ }
130
+ function alive(pid) {
131
+ try {
132
+ process.kill(pid, 0);
133
+ return true;
134
+ }
135
+ catch (err) {
136
+ // EPERM: the process exists, it is just not ours to signal.
137
+ return err.code === "EPERM";
138
+ }
139
+ }
140
+ /**
141
+ * Removes settings copies whose CLI is gone (killed by a signal before its exit
142
+ * cleanup ran). Liveness, not age: a session can run for days, and its Claude
143
+ * keeps reading its settings for as long as it runs.
144
+ */
145
+ export function sweepStaleSettings(tmp, isAlive = alive) {
146
+ let entries;
147
+ try {
148
+ entries = readdirSync(tmp);
149
+ }
150
+ catch {
151
+ return;
152
+ }
153
+ const uid = process.getuid?.();
154
+ for (const name of entries) {
155
+ if (!name.startsWith(PRIVATE_SETTINGS_PREFIX))
156
+ continue;
157
+ const pid = ownerPid(name);
158
+ if (pid === undefined || pid === process.pid || isAlive(pid))
159
+ continue;
160
+ const dir = path.join(tmp, name);
161
+ try {
162
+ if (uid === undefined || statSync(dir).uid === uid)
163
+ rmSync(dir, { recursive: true, force: true });
164
+ }
165
+ catch {
166
+ // Another process removed it, or it is not ours to read: leave it.
167
+ }
168
+ }
169
+ }
170
+ /**
171
+ * A private copy of merged settings for the life of this process. A user's
172
+ * settings file can hold secrets, so its contents never go into Claude's
173
+ * argv (visible in process listings); the copy is a 0600 file in a 0700
174
+ * directory. Copies are named by content, so a long headless session that
175
+ * re-plans every turn reuses one file per distinct value instead of piling
176
+ * them up. The directory goes when the CLI exits; one left by a signal is
177
+ * swept by a later launch once its process is gone. No signal handlers are
178
+ * installed: the launcher owns Ctrl+C, and re-raising a signal here would cut
179
+ * its shutdown short.
180
+ */
181
+ export function writePrivateSettings(json, tmp = os.tmpdir()) {
182
+ let dir = privateSettingsDirs.get(tmp);
183
+ if (!dir) {
184
+ sweepStaleSettings(tmp);
185
+ const created = mkdtempSync(path.join(tmp, `${PRIVATE_SETTINGS_PREFIX}${process.pid}-`));
186
+ chmodSync(created, 0o700);
187
+ process.once("exit", () => rmSync(created, { recursive: true, force: true }));
188
+ privateSettingsDirs.set(tmp, created);
189
+ dir = created;
190
+ }
191
+ const file = path.join(dir, `${createHash("sha256").update(json).digest("hex").slice(0, 16)}.json`);
192
+ if (!existsSync(file))
193
+ writeFileSync(file, json, { mode: 0o600 });
194
+ return file;
195
+ }
196
+ /**
197
+ * Adds settings to a Claude argv. Claude Code reads a single `--settings`, so
198
+ * the one already in the argv absorbs the new keys rather than a second flag
199
+ * silently replacing either. Inline JSON is merged in place. A settings file is
200
+ * read from Claude's launch directory and merged into a private copy whose path
201
+ * replaces it. A file that cannot be read stops the launch, since dropping it
202
+ * would lose the user's settings and dropping ours their MCP permissions.
203
+ */
204
+ export function withClaudeSettings(args, settings, io = {}) {
205
+ const read = io.read ?? ((f) => readFileSync(f, "utf8"));
206
+ const writePrivate = io.writePrivate ?? writePrivateSettings;
207
+ if (Object.keys(settings).length === 0)
208
+ return args;
209
+ for (let i = 0; i < args.length; i += 1) {
210
+ const a = args[i];
211
+ const inline = a.startsWith("--settings=");
212
+ if (a !== "--settings" && !inline)
213
+ continue;
214
+ const value = inline ? a.slice("--settings=".length) : args[i + 1];
215
+ if (value === undefined)
216
+ continue;
217
+ const isJSON = value.trim().startsWith("{");
218
+ let parsed;
219
+ try {
220
+ parsed = JSON.parse(isJSON ? value : read(path.resolve(io.cwd ?? process.cwd(), value)));
221
+ }
222
+ catch (err) {
223
+ throw new Error(`--settings ${value}: cannot be merged with native MCP permissions (${err.message})`);
224
+ }
225
+ if (!isObject(parsed))
226
+ throw new Error(`--settings ${value}: not a JSON object`);
227
+ const json = JSON.stringify(mergeSettings(parsed, settings));
228
+ const merged = isJSON ? json : writePrivate(json);
229
+ const out = [...args];
230
+ if (inline)
231
+ out[i] = `--settings=${merged}`;
232
+ else
233
+ out[i + 1] = merged;
234
+ return out;
235
+ }
236
+ return [...args, "--settings", JSON.stringify(settings)];
237
+ }
238
+ /** The permission settings an attachment contributes. */
239
+ export function attachmentSettings(mcp) {
240
+ const permissions = {};
241
+ if (mcp.allow.length)
242
+ permissions.allow = mcp.allow;
243
+ if (mcp.ask.length)
244
+ permissions.ask = mcp.ask;
245
+ return Object.keys(permissions).length ? { permissions } : {};
246
+ }
247
+ //# sourceMappingURL=claude-mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-mcp.js","sourceRoot":"","sources":["../../src/agents/claude-mcp.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { TOOL_DENY_HEADER } from "./claude-deny.js";
2
+ import { MCP_KEY_ENV, TOOL_SURFACE_HEADER, TOOL_SURFACE_MCP, attachmentSettings, withClaudeSettings } from "./claude-mcp.js";
2
3
  import { SESSION_HEADER, apiRootOf } from "./types.js";
3
4
  /**
4
5
  * Builds the `claude` invocation.
@@ -31,9 +32,15 @@ export function planClaude(input) {
31
32
  args.push("--model", input.model);
32
33
  if (input.prompt !== undefined)
33
34
  args.push("-p", input.prompt);
35
+ // Native MCP mode: the endpoint's tools as Claude Code's own servers, and
36
+ // the router told not to inject them a second time.
37
+ if (input.mcp)
38
+ args.push("--mcp-config", input.mcp.config);
34
39
  args.push(...input.passthrough);
40
+ const finalArgs = input.mcp ? withClaudeSettings(args, attachmentSettings(input.mcp), { cwd: input.cwd }) : args;
35
41
  const deny = input.toolDeny?.length ? `${TOOL_DENY_HEADER}: ${input.toolDeny.join(",")}` : undefined;
36
- const headers = [input.env.ANTHROPIC_CUSTOM_HEADERS?.trim(), `${SESSION_HEADER}: ${input.sessionId}`, deny]
42
+ const surface = input.mcp ? `${TOOL_SURFACE_HEADER}: ${TOOL_SURFACE_MCP}` : undefined;
43
+ const headers = [input.env.ANTHROPIC_CUSTOM_HEADERS?.trim(), `${SESSION_HEADER}: ${input.sessionId}`, deny, surface]
37
44
  .filter((h) => Boolean(h))
38
45
  .join("\n");
39
46
  const envSet = {
@@ -43,6 +50,8 @@ export function planClaude(input) {
43
50
  };
44
51
  if (input.model)
45
52
  envSet.ANTHROPIC_MODEL = input.model;
53
+ if (input.mcp)
54
+ envSet[MCP_KEY_ENV] = input.apiKey;
46
55
  // Claude Code's server-side auto-mode classifier needs the dangerous-tool-use
47
56
  // beta and `safeguards` field, which the router does not forward; a rejection
48
57
  // denies every auto-mode tool use. Decide locally unless the caller chose.
@@ -55,7 +64,7 @@ export function planClaude(input) {
55
64
  envSet.CLAUDE_CODE_GATEWAY_HINT_HEADERS = "1";
56
65
  return {
57
66
  command: "claude",
58
- args,
67
+ args: finalArgs,
59
68
  envSet,
60
69
  envUnset: ["ANTHROPIC_API_KEY"],
61
70
  installHint: "install Claude Code: npm i -g @anthropic-ai/claude-code (or see https://code.claude.com)",
@@ -1 +1 @@
1
- {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/agents/claude.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/agents/claude.ts"],"names":[],"mappings":"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"}
@@ -33,7 +33,15 @@ export interface LaunchOptions {
33
33
  * `claude -p` / `codex exec` turn in the session's thread until Ctrl+C.
34
34
  */
35
35
  headless?: boolean;
36
+ /**
37
+ * Claude only: attach the endpoint's tools as Claude Code MCP servers
38
+ * (`--mcp`) or keep them on the router (`--no-mcp`). Unset follows the
39
+ * endpoint's `claude_tool_surface` option, else the router.
40
+ */
41
+ mcp?: boolean;
36
42
  }
43
+ /** `--mcp` / `--no-mcp`, else the endpoint's `claude_tool_surface`, else the router. */
44
+ export declare function wantsMcp(options: Pick<LaunchOptions, "mcp">, endpoint: Pick<InferenceEndpoint, "providerOptions">): boolean;
37
45
  /** Interactive numbered picker; also accepts a slug or id. Exported for `heyditto endpoints pick`. */
38
46
  export declare function pickEndpoint(endpoints: InferenceEndpoint[], defaultSlug?: string): Promise<InferenceEndpoint>;
39
47
  /** Refuses to launch against an endpoint the backend marked inactive (e.g. awaiting the user's plan). */
@@ -14,6 +14,7 @@ import { findProfileEndpoint, repoDefaultProfile } from "../dittoconfig/profiles
14
14
  import { err as c } from "../ui.js";
15
15
  import { holdNotesForTui, releaseNotes, writeNote } from "../terminal-note.js";
16
16
  import { claudeDittoDenyRules } from "./claude-deny.js";
17
+ import { claudeMcpAttachment, fetchMcpCatalog } from "./claude-mcp.js";
17
18
  import { liftResumeFlags, planClaude } from "./claude.js";
18
19
  import { resolveClaudeResume, transcriptCwd } from "./claude-resume.js";
19
20
  import { findTranscriptById, listTranscriptsIn } from "./claude-transcripts.js";
@@ -29,6 +30,18 @@ import { claudeHookSettings, codexNotifyOverride, hookScriptPath, startHookServe
29
30
  import { loadPty, spawnInPty } from "../remote/pty.js";
30
31
  import { checkpointPush } from "../teleport/commands.js";
31
32
  const DRY_RUN_KEY = "ditto_inf_<minted-at-launch>";
33
+ /** `--mcp` / `--no-mcp`, else the endpoint's `claude_tool_surface`, else the router. */
34
+ export function wantsMcp(options, endpoint) {
35
+ if (options.mcp !== undefined)
36
+ return options.mcp;
37
+ return endpoint.providerOptions?.claude_tool_surface === "mcp";
38
+ }
39
+ function mcpBanner(mcp, dryRun) {
40
+ if (!mcp)
41
+ return dryRun ? "mcp (catalog read at launch)" : c("yellow", "router (MCP not attached)");
42
+ const asks = mcp.clientApprovals ? "Claude Code asks" : "Ditto holds gated calls";
43
+ return `mcp ${c("dim", `(${mcp.servers} servers, ${mcp.tools} tools; ${asks})`)}`;
44
+ }
32
45
  function log(line, options) {
33
46
  writeNote(`${c("dim", "ditto:")} ${line}\n`, options);
34
47
  }
@@ -462,6 +475,24 @@ export async function launchHarness(harness, rawArgs, options) {
462
475
  env: process.env,
463
476
  })
464
477
  : undefined;
478
+ // Native MCP mode reads the endpoint's tool catalog with the key just
479
+ // minted, so what Claude Code attaches is exactly what this key may reach.
480
+ // A catalog that cannot be read falls back to the router's own tools rather
481
+ // than launching an agent with none.
482
+ const wantMcp = harness === "claude" && wantsMcp(options, endpoint);
483
+ let mcp;
484
+ if (wantMcp && key?.key) {
485
+ try {
486
+ const attachment = claudeMcpAttachment(await fetchMcpCatalog(catalog.baseUrl, key.key, sessionId), sessionId, { yolo: options.yolo });
487
+ if (attachment.tools > 0)
488
+ mcp = attachment;
489
+ else
490
+ log(`${c("yellow", "native MCP unavailable")} (the catalog has no tools); the router keeps this endpoint's tools`);
491
+ }
492
+ catch (err) {
493
+ log(`${c("yellow", "native MCP unavailable")} (${err.message}); the router keeps this endpoint's tools`);
494
+ }
495
+ }
465
496
  const plan = planFor(harness, {
466
497
  baseUrl: catalog.baseUrl,
467
498
  apiKey: key?.key ?? DRY_RUN_KEY,
@@ -475,6 +506,8 @@ export async function launchHarness(harness, rawArgs, options) {
475
506
  plan: options.plan,
476
507
  passthrough,
477
508
  toolDeny: harness === "claude" ? claudeDittoDenyRules(cwd, process.env) : undefined,
509
+ mcp,
510
+ cwd,
478
511
  env: process.env,
479
512
  }, grokHome);
480
513
  const field = (name, value) => `${c("dim", `${name}=`)}${value}`;
@@ -485,6 +518,7 @@ export async function launchHarness(harness, rawArgs, options) {
485
518
  field("expires", `${expiresIn}${options.keepKey ? "" : c("dim", " (revoked on exit)")}`),
486
519
  budget !== undefined ? field("budget", `${budget.toLocaleString()} tokens`) : undefined,
487
520
  field("session", c("bold", sessionId)),
521
+ wantMcp ? field("tools", mcpBanner(mcp, options.dryRun)) : undefined,
488
522
  ]
489
523
  .filter(Boolean)
490
524
  .join(" ");
@@ -501,6 +535,10 @@ export async function launchHarness(harness, rawArgs, options) {
501
535
  cwd,
502
536
  endpoint: { id: endpoint.id, slug: endpoint.slug, model: endpoint.model },
503
537
  sessionId,
538
+ // A dry run reads no catalog, so the plan above is the router's; say
539
+ // which surface a real launch would ask for, separately.
540
+ toolSurface: mcp ? "mcp" : "router",
541
+ toolSurfaceRequested: wantMcp ? "mcp" : "router",
504
542
  key: { name: keyName, expiresIn, spendLimitTokens: budget ?? null },
505
543
  }, null, 2)}\n`);
506
544
  return;
@@ -537,6 +575,7 @@ export async function launchHarness(harness, rawArgs, options) {
537
575
  options,
538
576
  record: nextRecord,
539
577
  grokHome,
578
+ mcp,
540
579
  claudeStarted: harness === "claude" && !resumeLast ? Boolean(transcript) : undefined,
541
580
  });
542
581
  }
@@ -757,6 +796,8 @@ async function runHeadless(harness, ctx) {
757
796
  yolo: ctx.options.yolo,
758
797
  yellow: ctx.options.yellow,
759
798
  toolDeny: harness === "claude" ? claudeDittoDenyRules(ctx.cwd, process.env) : undefined,
799
+ mcp: ctx.mcp,
800
+ cwd: ctx.cwd,
760
801
  env: process.env,
761
802
  },
762
803
  prompt,