@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 +27 -1
- package/dist/agents/claude-deny.js +9 -0
- package/dist/agents/claude-deny.js.map +1 -1
- package/dist/agents/claude-mcp.d.ts +108 -0
- package/dist/agents/claude-mcp.js +247 -0
- package/dist/agents/claude-mcp.js.map +1 -0
- package/dist/agents/claude.js +11 -2
- package/dist/agents/claude.js.map +1 -1
- package/dist/agents/launch.d.ts +8 -0
- package/dist/agents/launch.js +41 -0
- package/dist/agents/launch.js.map +1 -1
- package/dist/agents/types.d.ts +5 -0
- package/dist/agents/types.js.map +1 -1
- package/dist/api.d.ts +4 -0
- package/dist/api.js.map +1 -1
- package/dist/commands.js +6 -0
- package/dist/commands.js.map +1 -1
- package/dist/dittoconfig/profiles.js +21 -1
- package/dist/dittoconfig/profiles.js.map +1 -1
- package/dist/remote/hooks.js +17 -1
- package/dist/remote/hooks.js.map +1 -1
- package/package.json +1 -1
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;
|
|
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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"}
|
package/dist/agents/claude.js
CHANGED
|
@@ -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
|
|
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":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAoC,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEzF;;;;;;;;GAQG;AACH,MAAM,UAAU,UAAU,CAAC,KAAgB;IACzC,MAAM,IAAI,GAAa,EAAE,CAAC;IAC1B,IAAI,KAAK,CAAC,IAAI;QAAE,IAAI,CAAC,IAAI,CAAC,gCAAgC,CAAC,CAAC;SACvD,IAAI,KAAK,CAAC,MAAM;QAAE,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,aAAa,CAAC,CAAC;SAChE,IAAI,KAAK,CAAC,IAAI;QAAE,IAAI,CAAC,IAAI,CAAC,mBAAmB,EAAE,MAAM,CAAC,CAAC;IAE5D,uEAAuE;IACvE,2EAA2E;IAC3E,iBAAiB;IACjB,MAAM,QAAQ,GAAG,KAAK,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,WAAW,IAAI,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,cAAc,IAAI,CAAC,CAAC,UAAU,CAAC,eAAe,CAAC,CAAC,CAAC;IACzJ,IAAI,KAAK,CAAC,QAAQ;QAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC,CAAC;SACrD,IAAI,KAAK,CAAC,UAAU;QAAE,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;SAC9C,IAAI,CAAC,QAAQ;QAAE,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IAE/D,IAAI,KAAK,CAAC,KAAK;QAAE,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC;IACnD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS;QAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAC9D,IAAI,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,WAAW,CAAC,CAAC;
|
|
1
|
+
{"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/agents/claude.ts"],"names":[],"mappings":"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"}
|
package/dist/agents/launch.d.ts
CHANGED
|
@@ -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). */
|
package/dist/agents/launch.js
CHANGED
|
@@ -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,
|