@wirecat/cli-core 0.466.0 → 0.468.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/CHANGELOG.md +64 -0
- package/README.md +4 -2
- package/dist/cli/bot/mcp.js +2 -1
- package/dist/cli/command-runs.d.ts +38 -10
- package/dist/cli/command-runs.js +51 -17
- package/dist/cli/config-command.d.ts +4 -0
- package/dist/cli/config-command.js +5 -5
- package/dist/cli/context.d.ts +3 -0
- package/dist/cli/context.js +15 -3
- package/dist/cli/effects.d.ts +9 -0
- package/dist/cli/effects.js +27 -0
- package/dist/cli/index.d.ts +3 -3
- package/dist/cli/index.js +2 -2
- package/dist/cli/messenger/context.js +1 -1
- package/dist/cli/messenger/observed.js +10 -2
- package/dist/cli/messenger/report.js +48 -3
- package/dist/cli/program.d.ts +1 -1
- package/dist/cli/program.js +12 -5
- package/dist/cli/settings.d.ts +24 -6
- package/dist/cli/settings.js +26 -4
- package/dist/cli/shared-command-exports.d.ts +12 -4
- package/dist/cli/skill-command.js +11 -3
- package/dist/cli/steps.d.ts +9 -0
- package/dist/cli/steps.js +16 -0
- package/dist/cli/store-settings.d.ts +6 -0
- package/dist/cli/store-settings.js +6 -0
- package/dist/entities/index.d.ts +39 -0
- package/dist/entities/recorded-run.d.ts +39 -0
- package/dist/entities/recorded-run.js +14 -1
- package/dist/mcp/agent.d.ts +5 -0
- package/dist/mcp/agent.js +3 -1
- package/dist/mcp/all-profiles.d.ts +2 -0
- package/dist/mcp/all-profiles.js +16 -3
- package/dist/mcp/bot/server.d.ts +2 -0
- package/dist/mcp/bot/server.js +9 -3
- package/dist/mcp/caller.d.ts +38 -0
- package/dist/mcp/caller.js +95 -0
- package/dist/mcp/http/oauth.js +5 -1
- package/dist/mcp/server.js +21 -8
- package/dist/mcp/surface.js +11 -2
- package/dist/mcp/tool.d.ts +4 -1
- package/dist/mcp/tool.js +21 -11
- package/dist/runs/events.d.ts +16 -4
- package/dist/runs/events.js +4 -0
- package/dist/runs/log-attributes.js +7 -1
- package/dist/runs/log-events.js +5 -0
- package/dist/runs/recording.d.ts +10 -3
- package/dist/runs/recording.js +59 -15
- package/dist/runs/run-context.d.ts +55 -4
- package/dist/runs/run-context.js +34 -3
- package/dist/runs/run.d.ts +37 -11
- package/dist/runs/run.js +201 -52
- package/dist/runs/scope.d.ts +12 -0
- package/dist/runs/scope.js +6 -0
- package/dist/services/embeddings-maintenance.js +4 -6
- package/dist/services/patience.js +5 -1
- package/package.json +1 -1
|
@@ -28,7 +28,20 @@ export const recordedRun = v.strictObject({
|
|
|
28
28
|
cli_command: v.optional(text),
|
|
29
29
|
argv: v.optional(v.array(text)),
|
|
30
30
|
unknown_command: v.optional(text),
|
|
31
|
-
agent: v.optional(v.strictObject({
|
|
31
|
+
agent: v.optional(v.strictObject({
|
|
32
|
+
kind: text,
|
|
33
|
+
session_id: v.nullable(text),
|
|
34
|
+
from_chat: v.optional(v.literal(true)),
|
|
35
|
+
client: v.optional(v.strictObject({ name: text, version: v.optional(text), id: v.optional(text) })),
|
|
36
|
+
})),
|
|
37
|
+
via: v.optional(v.picklist(["cli", "mcp-stdio", "mcp-http", "desktop"])),
|
|
38
|
+
mcp: v.optional(v.strictObject({
|
|
39
|
+
tool: text,
|
|
40
|
+
request_id: v.optional(text),
|
|
41
|
+
connection_id: v.optional(text),
|
|
42
|
+
proxied: v.optional(v.literal(true)),
|
|
43
|
+
})),
|
|
44
|
+
events_dropped: v.optional(count),
|
|
32
45
|
error: v.optional(v.strictObject({
|
|
33
46
|
class: text,
|
|
34
47
|
code: v.optional(text),
|
package/dist/mcp/agent.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export type { CommandAvailability } from "../cli/command-availability.js";
|
|
|
7
7
|
import type { RunOptions } from "../cli/program.js";
|
|
8
8
|
import type { StdinField } from "../cli/stdin-field.js";
|
|
9
9
|
import type { MessengerAdapter } from "../ports/messenger.js";
|
|
10
|
+
import type { RunCaller } from "../runs/run-context.js";
|
|
10
11
|
import { type Level, type PermissionKey } from "../sends/permissions.js";
|
|
11
12
|
/** The CLI's own `run` — a fresh program on every call — and its command tree as `commands --json` describes it. */
|
|
12
13
|
export interface AgentProgram {
|
|
@@ -32,6 +33,8 @@ export interface AgentContext {
|
|
|
32
33
|
stdin?: (path: readonly string[]) => StdinField | undefined;
|
|
33
34
|
/** More of the run's environment, read by the commands that look for it: the server's `spawnJob`, `embedders`. */
|
|
34
35
|
environment?: Readonly<Record<string, unknown>>;
|
|
36
|
+
/** Who made the call (`callerOf`); the run records it rather than this server's environment. */
|
|
37
|
+
caller?: RunCaller;
|
|
35
38
|
}
|
|
36
39
|
export type AgentAnswer = {
|
|
37
40
|
result: unknown;
|
|
@@ -67,6 +70,8 @@ export interface CatalogueOptions {
|
|
|
67
70
|
permissions?: CommandPermissions | undefined;
|
|
68
71
|
/** Whether a command changes what a later run may do (`grantsAuthority`): execution requires `mcp.authority`. */
|
|
69
72
|
authority?: (path: readonly string[]) => boolean;
|
|
73
|
+
/** Whether a command only fetches into this machine (`fetchesOnlyAt`): offered as a read. Unset, every write stays one. */
|
|
74
|
+
fetchOnly?: (path: readonly string[]) => boolean;
|
|
70
75
|
/** Arguments left out of every tool, by name. */
|
|
71
76
|
omit?: readonly string[];
|
|
72
77
|
/** Offer `offline` on commands that read the messenger: answer from the copy on this machine. */
|
package/dist/mcp/agent.js
CHANGED
|
@@ -8,6 +8,7 @@ import { commandAvailability } from "../cli/command-availability.js";
|
|
|
8
8
|
import { changesMessenger } from "../cli/effects.js";
|
|
9
9
|
import { lentConnection } from "../cli/messenger/serve-relay.js";
|
|
10
10
|
import { authorityAllowed, botApiAllowed } from "../sends/permissions.js";
|
|
11
|
+
import { currentCaller } from "./caller.js";
|
|
11
12
|
const TYPES = new Map(VOCABULARY.flags.map((flag) => [flag.name, flag.type]));
|
|
12
13
|
// `<name|id>` is one argument: its field is `name_or_id`.
|
|
13
14
|
const snake = (name) => name.replaceAll("|", "_or_").replace(/[^a-z0-9]+/gi, "_");
|
|
@@ -153,7 +154,7 @@ export const toolsFromCommands = (commands, options = {}) => {
|
|
|
153
154
|
path: info.path,
|
|
154
155
|
title: info.summary ?? info.description,
|
|
155
156
|
description: info.description,
|
|
156
|
-
writes: writes(info.effects),
|
|
157
|
+
writes: writes(info.effects) && options.fetchOnly?.(info.path) !== true,
|
|
157
158
|
input: schemaOf(fieldsOf(info, options)),
|
|
158
159
|
...(info.output ? { output: info.output } : {}),
|
|
159
160
|
}));
|
|
@@ -231,6 +232,7 @@ export const runCommandForAgent = async (program, path, input, ctx) => {
|
|
|
231
232
|
...(ctx.overrides?.length ? { overrides: ctx.overrides } : {}),
|
|
232
233
|
authority: levels !== undefined && authorityAllowed(levels),
|
|
233
234
|
botApi: levels !== undefined && botApiAllowed(levels),
|
|
235
|
+
...((ctx.caller ?? currentCaller()) ? { caller: ctx.caller ?? currentCaller() } : {}),
|
|
234
236
|
},
|
|
235
237
|
// Opened only when the command asks: a read the store answers never connects.
|
|
236
238
|
...(lend
|
|
@@ -25,6 +25,8 @@ export interface AllProfilesOptions {
|
|
|
25
25
|
idleMs?: number;
|
|
26
26
|
/** A child's stderr, line by line; never this process's stdout, which carries the protocol. */
|
|
27
27
|
note?: (line: string) => void;
|
|
28
|
+
/** This server's environment, which names the agent that started it on stdio; `process.env` when left out. */
|
|
29
|
+
env?: NodeJS.ProcessEnv;
|
|
28
30
|
}
|
|
29
31
|
/**
|
|
30
32
|
* The child's tool with the one field that picks the profile, required and without a default. Each profile carries
|
package/dist/mcp/all-profiles.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
3
4
|
import { StdioServerTransport, serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
5
|
+
import { CALLER_META_KEY, callerOf, PROXY_CLIENT_NAME } from "./caller.js";
|
|
4
6
|
import { answeredWire } from "./stdio-wire.js";
|
|
5
7
|
const PROTOCOL = "2025-06-18";
|
|
6
8
|
const START_MS = 30_000;
|
|
@@ -82,7 +84,7 @@ const startChild = (spec, note, onExit) => {
|
|
|
82
84
|
return request("initialize", {
|
|
83
85
|
protocolVersion: PROTOCOL,
|
|
84
86
|
capabilities: {},
|
|
85
|
-
clientInfo: { name:
|
|
87
|
+
clientInfo: { name: PROXY_CLIENT_NAME, version: "1" },
|
|
86
88
|
})
|
|
87
89
|
.then(() => {
|
|
88
90
|
send({ method: "notifications/initialized" });
|
|
@@ -202,7 +204,12 @@ export const allProfilesServer = (options) => {
|
|
|
202
204
|
? []
|
|
203
205
|
: (await tools(profiles)).map((one) => withProfile(one, profiles, options.name, signedIn)),
|
|
204
206
|
}));
|
|
205
|
-
|
|
207
|
+
const callers = {
|
|
208
|
+
env: options.env ?? process.env,
|
|
209
|
+
client: () => server.server.getClientVersion(),
|
|
210
|
+
connectionId: randomUUID(),
|
|
211
|
+
};
|
|
212
|
+
server.server.setRequestHandler("tools/call", async (request, ctx) => {
|
|
206
213
|
const { name, arguments: given = {}, _meta } = request.params;
|
|
207
214
|
const { profile, ...rest } = given;
|
|
208
215
|
if (typeof profile !== "string" || !profiles.includes(profile))
|
|
@@ -210,7 +217,13 @@ export const allProfilesServer = (options) => {
|
|
|
210
217
|
if (!signedIn(profile))
|
|
211
218
|
return refusal(options.signIn?.(profile) ?? `profile ${profile} is not signed in.`);
|
|
212
219
|
try {
|
|
213
|
-
return (await children.use(profile, (child) =>
|
|
220
|
+
return (await children.use(profile, (child) =>
|
|
221
|
+
// Children are shared by every session, so each call names its own caller.
|
|
222
|
+
child.request("tools/call", {
|
|
223
|
+
name,
|
|
224
|
+
arguments: rest,
|
|
225
|
+
_meta: { ..._meta, [CALLER_META_KEY]: callerOf(ctx, name, callers) },
|
|
226
|
+
})));
|
|
214
227
|
}
|
|
215
228
|
catch (error) {
|
|
216
229
|
return refusal(`profile ${profile}: ${error instanceof Error ? error.message : String(error)}`);
|
package/dist/mcp/bot/server.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface BotServerOptions {
|
|
|
16
16
|
keyOf?: (path: readonly string[]) => PermissionKey | null | undefined;
|
|
17
17
|
/** Whether a command changes what a later run may do; such a command requires `mcp.authority` to execute. */
|
|
18
18
|
authority?: (path: readonly string[]) => boolean;
|
|
19
|
+
/** Whether a command only fetches into this machine (`fetchesOnlyAt`): offered through the read tool. */
|
|
20
|
+
fetchOnly?: (path: readonly string[]) => boolean;
|
|
19
21
|
env: NodeJS.ProcessEnv;
|
|
20
22
|
skill?: URL;
|
|
21
23
|
}
|
package/dist/mcp/bot/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
3
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
3
4
|
import { mcpErrorResult } from "@wirecat/cli-base";
|
|
@@ -9,6 +10,7 @@ import { itemOf } from "../../cli/envelope.js";
|
|
|
9
10
|
import { wholeList } from "../../cli/messenger/chat-answers.js";
|
|
10
11
|
import { MESSENGER_CHANGE_REFUSAL, readOnlyLockOf, readOnlyRefusal } from "../../ports/app.js";
|
|
11
12
|
import { runCommandForAgent, toolsFromCommands } from "../agent.js";
|
|
13
|
+
import { callerOf } from "../caller.js";
|
|
12
14
|
import { normal, registerCommands } from "../surface.js";
|
|
13
15
|
import { agentJson } from "../text.js";
|
|
14
16
|
import { answered } from "../tool.js";
|
|
@@ -31,6 +33,7 @@ export const createBotServer = (options) => {
|
|
|
31
33
|
readOnly: readOnlyLockOf(bot.app, env) !== undefined,
|
|
32
34
|
...(options.keyOf ? { keyOf: options.keyOf } : {}),
|
|
33
35
|
authority,
|
|
36
|
+
...(options.fetchOnly ? { fetchOnly: options.fetchOnly } : {}),
|
|
34
37
|
offline: true,
|
|
35
38
|
// Another bot's copy is this bot's to read only where `read_other_bots` says so.
|
|
36
39
|
...(settings.readOtherBots ? {} : { omit: ["all_bots", "bots"] }),
|
|
@@ -48,7 +51,7 @@ export const createBotServer = (options) => {
|
|
|
48
51
|
const skill = options.skill ? skillResource(bot.app, options.skill) : undefined;
|
|
49
52
|
// One at a time: the commands share the journal, the registry and the local copy.
|
|
50
53
|
let queue = Promise.resolve();
|
|
51
|
-
const invoke = (path, input) => {
|
|
54
|
+
const invoke = (path, input, caller) => {
|
|
52
55
|
const next = queue.then(() => runCommandForAgent(program, path, path[0] === "config" ? { ...input, bot: true } : input, {
|
|
53
56
|
profile: settings.profile,
|
|
54
57
|
readOnly: readOnlyLockOf(bot.app, env) !== undefined,
|
|
@@ -56,11 +59,13 @@ export const createBotServer = (options) => {
|
|
|
56
59
|
levels,
|
|
57
60
|
...(options.keyOf ? { keyOf: options.keyOf } : {}),
|
|
58
61
|
authority,
|
|
62
|
+
...(caller ? { caller } : {}),
|
|
59
63
|
}));
|
|
60
64
|
queue = next.catch(() => undefined);
|
|
61
65
|
return next;
|
|
62
66
|
};
|
|
63
67
|
const build = () => {
|
|
68
|
+
const callers = { env, client: () => server.server.getClientVersion(), connectionId: randomUUID() };
|
|
64
69
|
const server = new McpServer({ name: `${command}-bot-${settings.profile}`, version: bot.app.version }, {
|
|
65
70
|
instructions: botInstructions({
|
|
66
71
|
command,
|
|
@@ -82,8 +87,9 @@ export const createBotServer = (options) => {
|
|
|
82
87
|
writes: tool.writes,
|
|
83
88
|
availability: tool.availability,
|
|
84
89
|
input: tool.input,
|
|
85
|
-
run: async (args) => {
|
|
86
|
-
const
|
|
90
|
+
run: async (args, ctx) => {
|
|
91
|
+
const caller = callerOf(ctx, `${command}_${tool.command.replaceAll(" ", "_")}`, callers);
|
|
92
|
+
const answer = await invoke(tool.path, args, caller);
|
|
87
93
|
return "error" in answer
|
|
88
94
|
? mcpErrorResult(JSON.parse(agentJson(answer.error)))
|
|
89
95
|
: answered(body(tool.command, answer.result));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type RunCaller } from "../runs/run-context.js";
|
|
2
|
+
export { currentCaller, withCaller } from "../runs/run-context.js";
|
|
3
|
+
/** The `_meta` key the every-profile server names the real caller under, for the profile server it passes a call to. */
|
|
4
|
+
export declare const CALLER_META_KEY = "wirecat.dev/caller";
|
|
5
|
+
/** The name the every-profile server gives itself in `initialize`; only a connection from it may name a caller. */
|
|
6
|
+
export declare const PROXY_CLIENT_NAME = "all-profiles";
|
|
7
|
+
/** What a tool call's handler is given, reduced to what identifies the caller. */
|
|
8
|
+
export interface CallContext {
|
|
9
|
+
sessionId?: string | undefined;
|
|
10
|
+
mcpReq?: {
|
|
11
|
+
id?: string | number;
|
|
12
|
+
_meta?: Record<string, unknown> | undefined;
|
|
13
|
+
} | undefined;
|
|
14
|
+
http?: {
|
|
15
|
+
authInfo?: {
|
|
16
|
+
clientId: string;
|
|
17
|
+
extra?: Record<string, unknown> | undefined;
|
|
18
|
+
} | undefined;
|
|
19
|
+
} | undefined;
|
|
20
|
+
}
|
|
21
|
+
export interface CallerSource {
|
|
22
|
+
/** This server's environment: the coding agent that started a stdio server. Never read for an HTTP call. */
|
|
23
|
+
env: NodeJS.ProcessEnv;
|
|
24
|
+
/** The client's `initialize`, when this connection had one. */
|
|
25
|
+
client?: () => {
|
|
26
|
+
name: string;
|
|
27
|
+
version?: string;
|
|
28
|
+
} | undefined;
|
|
29
|
+
/** One id per stdio connection, made when the server starts. */
|
|
30
|
+
connectionId?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Who made one MCP call. Over HTTP the server's own environment is somebody else's — whoever started `mcp --http` —
|
|
34
|
+
* so the caller is the OAuth client (its registered name and a hash of its id) and the HTTP session, if any. Over
|
|
35
|
+
* stdio the agent that started the server is the caller, and `initialize` names its client. A call the every-profile
|
|
36
|
+
* server passed on carries the caller it worked out, trusted only on a connection that server opened.
|
|
37
|
+
*/
|
|
38
|
+
export declare const callerOf: (ctx: CallContext | undefined, tool: string, source: CallerSource) => RunCaller;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { CLIENT_INFO_META_KEY } from "@modelcontextprotocol/server";
|
|
2
|
+
import { agentOf, clientHash } from "../runs/run-context.js";
|
|
3
|
+
export { currentCaller, withCaller } from "../runs/run-context.js";
|
|
4
|
+
/** The `_meta` key the every-profile server names the real caller under, for the profile server it passes a call to. */
|
|
5
|
+
export const CALLER_META_KEY = "wirecat.dev/caller";
|
|
6
|
+
/** The name the every-profile server gives itself in `initialize`; only a connection from it may name a caller. */
|
|
7
|
+
export const PROXY_CLIENT_NAME = "all-profiles";
|
|
8
|
+
const NAME = /^[\w .@()/+-]{1,64}$/;
|
|
9
|
+
const plain = (value) => typeof value === "string" && NAME.test(value.trim()) ? value.trim() : undefined;
|
|
10
|
+
const clientFrom = (value) => {
|
|
11
|
+
if (!value || typeof value !== "object")
|
|
12
|
+
return undefined;
|
|
13
|
+
const { name, version } = value;
|
|
14
|
+
const named = plain(name);
|
|
15
|
+
if (!named)
|
|
16
|
+
return undefined;
|
|
17
|
+
const versioned = plain(version);
|
|
18
|
+
return { name: named, ...(versioned ? { version: versioned } : {}) };
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Who made one MCP call. Over HTTP the server's own environment is somebody else's — whoever started `mcp --http` —
|
|
22
|
+
* so the caller is the OAuth client (its registered name and a hash of its id) and the HTTP session, if any. Over
|
|
23
|
+
* stdio the agent that started the server is the caller, and `initialize` names its client. A call the every-profile
|
|
24
|
+
* server passed on carries the caller it worked out, trusted only on a connection that server opened.
|
|
25
|
+
*/
|
|
26
|
+
export const callerOf = (ctx, tool, source) => {
|
|
27
|
+
const meta = ctx?.mcpReq?._meta;
|
|
28
|
+
const requestId = ctx?.mcpReq?.id;
|
|
29
|
+
const initialized = source.client?.();
|
|
30
|
+
const proxied = initialized?.name === PROXY_CLIENT_NAME ? passedOn(meta?.[CALLER_META_KEY], tool) : undefined;
|
|
31
|
+
if (proxied)
|
|
32
|
+
return proxied;
|
|
33
|
+
const client = clientFrom(meta?.[CLIENT_INFO_META_KEY]) ?? clientFrom(initialized);
|
|
34
|
+
const mcp = (connection) => ({
|
|
35
|
+
tool,
|
|
36
|
+
...(requestId === undefined ? {} : { request_id: String(requestId) }),
|
|
37
|
+
...(connection ? { connection_id: connection } : {}),
|
|
38
|
+
});
|
|
39
|
+
const auth = ctx?.http?.authInfo;
|
|
40
|
+
if (auth) {
|
|
41
|
+
const registered = plain(auth.extra?.clientName);
|
|
42
|
+
return {
|
|
43
|
+
via: "mcp-http",
|
|
44
|
+
agent: {
|
|
45
|
+
kind: "mcp-client",
|
|
46
|
+
session_id: ctx?.sessionId ?? null,
|
|
47
|
+
client: {
|
|
48
|
+
name: registered ?? client?.name ?? "unknown",
|
|
49
|
+
...(client?.version ? { version: client.version } : {}),
|
|
50
|
+
id: clientHash(auth.clientId),
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
mcp: mcp(ctx?.sessionId),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const agent = agentOf(source.env) ?? { kind: "mcp-client", session_id: null };
|
|
57
|
+
return { via: "mcp-stdio", agent: { ...agent, ...(client ? { client } : {}) }, mcp: mcp(source.connectionId) };
|
|
58
|
+
};
|
|
59
|
+
/** The caller the every-profile server named, field by field: nothing else it sent reaches a run. */
|
|
60
|
+
const passedOn = (value, tool) => {
|
|
61
|
+
if (!value || typeof value !== "object")
|
|
62
|
+
return undefined;
|
|
63
|
+
const { via, agent, mcp } = value;
|
|
64
|
+
if (via !== "mcp-stdio" && via !== "mcp-http")
|
|
65
|
+
return undefined;
|
|
66
|
+
const a = agent && typeof agent === "object" ? agent : undefined;
|
|
67
|
+
const kind = plain(a?.kind);
|
|
68
|
+
const session = typeof a?.session_id === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(a.session_id) ? a.session_id : null;
|
|
69
|
+
const client = clientFrom(a?.client);
|
|
70
|
+
const id = a?.client && typeof a.client === "object" ? a.client.id : undefined;
|
|
71
|
+
const m = mcp && typeof mcp === "object" ? mcp : {};
|
|
72
|
+
const request = plain(m.request_id);
|
|
73
|
+
const connection = typeof m.connection_id === "string" && /^[\w-]{1,128}$/.test(m.connection_id) ? m.connection_id : undefined;
|
|
74
|
+
return {
|
|
75
|
+
via,
|
|
76
|
+
...(kind
|
|
77
|
+
? {
|
|
78
|
+
agent: {
|
|
79
|
+
kind,
|
|
80
|
+
session_id: session,
|
|
81
|
+
...(a?.from_chat === true ? { from_chat: true } : {}),
|
|
82
|
+
...(client
|
|
83
|
+
? { client: { ...client, ...(typeof id === "string" && /^[0-9a-f]{8}$/.test(id) ? { id } : {}) } }
|
|
84
|
+
: {}),
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
: {}),
|
|
88
|
+
mcp: {
|
|
89
|
+
tool,
|
|
90
|
+
...(request ? { request_id: request } : {}),
|
|
91
|
+
...(connection ? { connection_id: connection } : {}),
|
|
92
|
+
proxied: true,
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
};
|
package/dist/mcp/http/oauth.js
CHANGED
|
@@ -258,15 +258,19 @@ export const ownerLogin = ({ issuer, resource, file, appName, onCode, now = () =
|
|
|
258
258
|
const verifier = {
|
|
259
259
|
verifyAccessToken: async (presented) => {
|
|
260
260
|
const seconds = Math.floor(now() / 1000);
|
|
261
|
-
const
|
|
261
|
+
const stored = store.read();
|
|
262
|
+
const found = stored.tokens.find((one) => one.kind === "access" && one.hash === hash(presented));
|
|
262
263
|
if (!found || found.expiresAt <= seconds)
|
|
263
264
|
throw new OAuthError(OAuthErrorCode.InvalidToken, "the token is unknown or expired");
|
|
265
|
+
// The name the app registered with, so a run can say which app called; the id itself never leaves here unhashed.
|
|
266
|
+
const clientName = stored.clients.find((one) => one.id === found.clientId)?.name;
|
|
264
267
|
return {
|
|
265
268
|
token: presented,
|
|
266
269
|
clientId: found.clientId,
|
|
267
270
|
scopes: [],
|
|
268
271
|
expiresAt: found.expiresAt,
|
|
269
272
|
resource,
|
|
273
|
+
...(clientName ? { extra: { clientName } } : {}),
|
|
270
274
|
};
|
|
271
275
|
},
|
|
272
276
|
};
|
package/dist/mcp/server.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
2
3
|
import { StdioServerTransport, serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
3
4
|
import { CliError, mcpErrorResult } from "@wirecat/cli-base";
|
|
@@ -7,7 +8,7 @@ import * as v from "valibot";
|
|
|
7
8
|
import { findCommand } from "../cli/command-contract.js";
|
|
8
9
|
import { environmentOf } from "../cli/context.js";
|
|
9
10
|
import { refuseWriteBeforeConnecting } from "../cli/early-refusal.js";
|
|
10
|
-
import { changesAuthorityAt, judgedTargets } from "../cli/effects.js";
|
|
11
|
+
import { changesAuthorityAt, fetchesOnlyAt, judgedTargets } from "../cli/effects.js";
|
|
11
12
|
import { itemOf } from "../cli/envelope.js";
|
|
12
13
|
import { DEFAULT_OUTPUT_BYTES } from "../cli/execution.js";
|
|
13
14
|
import { recalledAccount } from "../cli/messenger/accounts.js";
|
|
@@ -25,6 +26,7 @@ import { levelFor, readKeysForCommand } from "../sends/permissions.js";
|
|
|
25
26
|
import { openStore } from "../store/store.js";
|
|
26
27
|
import { runCommandForAgent, toolsFromCommands } from "./agent.js";
|
|
27
28
|
import { agentLog } from "./agent-log.js";
|
|
29
|
+
import { callerOf, withCaller } from "./caller.js";
|
|
28
30
|
import { mcpHttpMarker } from "./http/state.js";
|
|
29
31
|
import { instructions } from "./instructions.js";
|
|
30
32
|
import { registerPrompts } from "./prompts.js";
|
|
@@ -125,6 +127,7 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
125
127
|
readOnly: readOnlyLockOf(app, context.env) !== undefined,
|
|
126
128
|
keyOf,
|
|
127
129
|
authority,
|
|
130
|
+
fetchOnly: fetchesOnlyAt(root),
|
|
128
131
|
offline: true,
|
|
129
132
|
stdin,
|
|
130
133
|
}).map((one) => "sync_first" in one.input.entries && !syncAllowed(one.path)
|
|
@@ -195,7 +198,7 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
195
198
|
* One command at a time, on the session's connection, opened only if the command asks for it. A failure that may
|
|
196
199
|
* have been the connection's drops it, as the session does for its own calls.
|
|
197
200
|
*/
|
|
198
|
-
const invoke = (one, input, env) => new Promise((resolve) => {
|
|
201
|
+
const invoke = (one, input, env, caller) => new Promise((resolve) => {
|
|
199
202
|
let finish;
|
|
200
203
|
let held;
|
|
201
204
|
const connection = () => new Promise((lend, refuse) => {
|
|
@@ -219,6 +222,7 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
219
222
|
stdin,
|
|
220
223
|
connection,
|
|
221
224
|
environment: agentEnvironment,
|
|
225
|
+
caller,
|
|
222
226
|
}).then(async (answer) => {
|
|
223
227
|
finish?.("error" in answer && !harmless(answer.error.error.code));
|
|
224
228
|
await held?.catch(() => undefined);
|
|
@@ -226,12 +230,12 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
226
230
|
});
|
|
227
231
|
});
|
|
228
232
|
let queue = Promise.resolve();
|
|
229
|
-
const queued = (one, input, env) => {
|
|
230
|
-
const next = queue.then(() => invoke(one, input, env));
|
|
233
|
+
const queued = (one, input, env, caller) => {
|
|
234
|
+
const next = queue.then(() => invoke(one, input, env, caller));
|
|
231
235
|
queue = next.catch(() => undefined);
|
|
232
236
|
return next;
|
|
233
237
|
};
|
|
234
|
-
const asCommand = (one) => ({
|
|
238
|
+
const asCommand = (one, source) => ({
|
|
235
239
|
title: one.title,
|
|
236
240
|
description: one.description,
|
|
237
241
|
writes: one.writes,
|
|
@@ -243,7 +247,9 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
243
247
|
// The caller's trace reaches the run, its send journal lines and its record, as `TRACEPARENT` does.
|
|
244
248
|
const traceparent = ctx?.mcpReq?._meta?.traceparent;
|
|
245
249
|
const env = typeof traceparent === "string" ? { ...context.env, TRACEPARENT: traceparent } : context.env;
|
|
246
|
-
const
|
|
250
|
+
const caller = callerOf(ctx, keyOfCommand(one.command), source);
|
|
251
|
+
// The connection the session opens for this call is recorded under the same caller.
|
|
252
|
+
const answer = await withCaller(caller, () => queued(one, args, env, caller));
|
|
247
253
|
const result = "error" in answer
|
|
248
254
|
? mcpErrorResult(JSON.parse(agentJson(answer.error)))
|
|
249
255
|
: (() => {
|
|
@@ -266,6 +272,12 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
266
272
|
},
|
|
267
273
|
});
|
|
268
274
|
const build = () => {
|
|
275
|
+
// Built once for a stdio connection; over HTTP the session or the OAuth client identifies the caller instead.
|
|
276
|
+
const callers = {
|
|
277
|
+
env: context.env,
|
|
278
|
+
client: () => server.server.getClientVersion(),
|
|
279
|
+
connectionId: randomUUID(),
|
|
280
|
+
};
|
|
269
281
|
const server = new McpServer({ name: app.command, version: app.version }, {
|
|
270
282
|
instructions: instructions({
|
|
271
283
|
command: app.command,
|
|
@@ -305,11 +317,12 @@ export const createServer = (command, context, messenger, sessionOptions) => {
|
|
|
305
317
|
env: context.env,
|
|
306
318
|
...(context.keyring ? { keyring: context.keyring } : {}),
|
|
307
319
|
embedders,
|
|
308
|
-
history: settings.
|
|
320
|
+
history: settings.record,
|
|
309
321
|
},
|
|
322
|
+
callers,
|
|
310
323
|
};
|
|
311
324
|
registerCommands(server, app.command, {
|
|
312
|
-
...Object.fromEntries(commands.map((one) => [one.command, asCommand(one)])),
|
|
325
|
+
...Object.fromEntries(commands.map((one) => [one.command, asCommand(one, callers)])),
|
|
313
326
|
...Object.fromEntries(Object.entries(surfaceCommands({ ...pictures, status }, registration)).map(([name, one]) => [
|
|
314
327
|
name,
|
|
315
328
|
{
|
package/dist/mcp/surface.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { toJsonSchema, toStandardJsonSchema } from "@valibot/to-json-schema";
|
|
2
3
|
import { CliError } from "@wirecat/cli-base";
|
|
3
4
|
import * as v from "valibot";
|
|
4
5
|
import { answerSchema, listOf } from "../cli/envelope.js";
|
|
5
6
|
import { ENTITIES } from "../entities/index.js";
|
|
7
|
+
import { callerOf, currentCaller, withCaller } from "./caller.js";
|
|
6
8
|
import { agentArguments } from "./text.js";
|
|
7
9
|
import { answered, commandOf, entryRunner, failed, inputOf, outputSchemaOf, READ, syncAllowedFor, UNTRUSTED, UNTRUSTED_NEUTRAL, WRITE, } from "./tool.js";
|
|
8
10
|
const MATCHES = 8;
|
|
@@ -113,6 +115,12 @@ isMessenger) => {
|
|
|
113
115
|
});
|
|
114
116
|
return answered(list);
|
|
115
117
|
});
|
|
118
|
+
// A CLI's own server runs in its own process: its environment names the agent that started it on stdio.
|
|
119
|
+
const callers = {
|
|
120
|
+
env: process.env,
|
|
121
|
+
client: () => server.server.getClientVersion(),
|
|
122
|
+
connectionId: randomUUID(),
|
|
123
|
+
};
|
|
116
124
|
const runner = (writing) => async ({ command: asked, arguments: given }, ctx) => {
|
|
117
125
|
try {
|
|
118
126
|
const command = byNormal.get(normal(asked));
|
|
@@ -125,14 +133,15 @@ isMessenger) => {
|
|
|
125
133
|
if (one.writes !== writing)
|
|
126
134
|
throw new CliError("validation_error", `"${command}" ${writing ? "only reads" : "writes"}: call it with ${prefix}_${writing ? "read" : "write"}`);
|
|
127
135
|
const args = writing ? agentArguments(given ?? {}) : (given ?? {});
|
|
136
|
+
const caller = currentCaller() ?? callerOf(ctx, command.replace(/[ -]/g, "_"), callers);
|
|
128
137
|
if (one.checked)
|
|
129
|
-
return runnableOnly(await one.run(args, ctx), (named) => byNormal.has(normal(named)));
|
|
138
|
+
return runnableOnly(await withCaller(caller, () => one.run(args, ctx)), (named) => byNormal.has(normal(named)));
|
|
130
139
|
const parsed = v.safeParse(v.strictObject(one.input.entries), args);
|
|
131
140
|
if (!parsed.success)
|
|
132
141
|
throw new CliError("validation_error", `${command}: ${v.summarize(parsed.issues)}`, {
|
|
133
142
|
arguments: toJsonSchema(one.input),
|
|
134
143
|
});
|
|
135
|
-
return runnableOnly(await one.run(parsed.output, ctx), (named) => byNormal.has(normal(named)));
|
|
144
|
+
return runnableOnly(await withCaller(caller, () => one.run(parsed.output, ctx)), (named) => byNormal.has(normal(named)));
|
|
136
145
|
}
|
|
137
146
|
catch (error) {
|
|
138
147
|
return failed(error, errors);
|
package/dist/mcp/tool.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { SendGuard } from "../sends/guard.js";
|
|
|
13
13
|
import { type PermissionKey } from "../sends/permissions.js";
|
|
14
14
|
import { type Services } from "../services/index.js";
|
|
15
15
|
import type { AccountKey, MessageStore } from "../store/store.js";
|
|
16
|
+
import { type CallerSource } from "./caller.js";
|
|
16
17
|
import type { MessengerSession } from "./session.js";
|
|
17
18
|
export declare const limit: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.MaxValueAction<number, 100, undefined>, v.DescriptionAction<number, "how many">]>, undefined>;
|
|
18
19
|
export declare const page: v.OptionalSchema<v.SchemaWithPipe<readonly [v.NumberSchema<undefined>, v.IntegerAction<number, undefined>, v.MinValueAction<number, 1, undefined>, v.DescriptionAction<number, "which page, from 1">]>, undefined>;
|
|
@@ -34,7 +35,7 @@ export interface Defaults {
|
|
|
34
35
|
limit: number;
|
|
35
36
|
guard: SendGuard;
|
|
36
37
|
/** The profile's own entries, for a tool reading a setting of its own — `transcribeWith`. */
|
|
37
|
-
settings: Pick<Settings, "configured" | "shared" | "profile"> & Partial<Pick<Settings, "permissions" | "searchCatchUp" | "pictureSearch" | "searchReranker" | "offline">> & AISettings;
|
|
38
|
+
settings: Pick<Settings, "configured" | "shared" | "profile"> & Partial<Pick<Settings, "permissions" | "searchCatchUp" | "pictureSearch" | "searchReranker" | "offline" | "record" | "keepFailedRuns" | "keepRunsForDays" | "keepSuccessfulRunsForDays" | "runsSizeLimitMb">> & AISettings;
|
|
38
39
|
env: NodeJS.ProcessEnv;
|
|
39
40
|
keyring?: KeyringStore;
|
|
40
41
|
/** The server's open models, kept between `search_conversations` calls. */
|
|
@@ -123,6 +124,8 @@ export interface Registration {
|
|
|
123
124
|
log?: (call: ToolCall) => Promise<void>;
|
|
124
125
|
/** The CLI's commands, by which an answer's next steps are judged safe to run unasked. */
|
|
125
126
|
targets?: NonNullable<StepInput["targets"]>;
|
|
127
|
+
/** How a call's caller is worked out on this connection (`callerOf`); without it the environment's agent is kept. */
|
|
128
|
+
callers?: CallerSource;
|
|
126
129
|
}
|
|
127
130
|
export type ToolTier = "read" | "draft" | "write-private" | "write-public" | "destructive" | "admin";
|
|
128
131
|
export interface ToolCall {
|
package/dist/mcp/tool.js
CHANGED
|
@@ -9,13 +9,14 @@ import { MAX_BUFFERED_INPUT } from "../cli/input-policy.js";
|
|
|
9
9
|
import { refuseLacking } from "../cli/messenger/lacks.js";
|
|
10
10
|
import { ENTITIES } from "../entities/index.js";
|
|
11
11
|
import { failureContext } from "../errors/reasons.js";
|
|
12
|
-
import {
|
|
12
|
+
import { startRecording } from "../runs/recording.js";
|
|
13
13
|
import { newRunId } from "../runs/run.js";
|
|
14
|
-
import { runContextOf } from "../runs/run-context.js";
|
|
14
|
+
import { keepsSuccess, runContextOf } from "../runs/run-context.js";
|
|
15
15
|
import { inRun } from "../runs/scope.js";
|
|
16
16
|
import { keyForCommand, levelFor, readKeysForCommand } from "../sends/permissions.js";
|
|
17
17
|
import { onlineDeps, storeModeDeps } from "../services/deps.js";
|
|
18
18
|
import { servicesFor } from "../services/index.js";
|
|
19
|
+
import { callerOf, currentCaller, withCaller } from "./caller.js";
|
|
19
20
|
import { agentJson } from "./text.js";
|
|
20
21
|
export const limit = v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.maxValue(100), v.description("how many")));
|
|
21
22
|
export const page = v.optional(v.pipe(v.number(), v.integer(), v.minValue(1), v.description("which page, from 1")));
|
|
@@ -104,19 +105,25 @@ export const entryRunner = (registration) => {
|
|
|
104
105
|
const command = `mcp ${commandOf(key, definition)}`;
|
|
105
106
|
const traceId = traceIdOf(ctx?.mcpReq?._meta?.traceparent);
|
|
106
107
|
const app = registration.messenger?.app;
|
|
107
|
-
const
|
|
108
|
+
const caller = registration.callers ? callerOf(ctx, key, registration.callers) : currentCaller();
|
|
109
|
+
const env = registration.defaults.env ?? process.env;
|
|
110
|
+
const settings = registration.defaults.settings;
|
|
111
|
+
const recording = key !== "runs_search" && settings.keepFailedRuns !== false && app?.appName
|
|
108
112
|
? startRecording({
|
|
109
113
|
app,
|
|
110
114
|
command,
|
|
111
|
-
...(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
env: registration.defaults.env,
|
|
115
|
-
record: false,
|
|
115
|
+
...(settings.profile === undefined ? {} : { profile: settings.profile }),
|
|
116
|
+
env,
|
|
117
|
+
record: keepsSuccess({ record: settings.record ?? true, fromFlag: false, env, caller }),
|
|
116
118
|
keepFailed: true,
|
|
117
119
|
trace: false,
|
|
118
120
|
format: "json",
|
|
119
|
-
context: runContextOf({ bin: app.command, command, env
|
|
121
|
+
context: runContextOf({ bin: app.command, command, env, caller }),
|
|
122
|
+
...(settings.keepRunsForDays === undefined ? {} : { keepDays: settings.keepRunsForDays }),
|
|
123
|
+
...(settings.keepSuccessfulRunsForDays === undefined
|
|
124
|
+
? {}
|
|
125
|
+
: { keepSuccessDays: settings.keepSuccessfulRunsForDays }),
|
|
126
|
+
...(settings.runsSizeLimitMb === undefined ? {} : { maxBytes: settings.runsSizeLimitMb * 1024 * 1024 }),
|
|
120
127
|
...(traceId === undefined ? {} : { traceId }),
|
|
121
128
|
})
|
|
122
129
|
: undefined;
|
|
@@ -127,8 +134,9 @@ export const entryRunner = (registration) => {
|
|
|
127
134
|
startedAt,
|
|
128
135
|
...(profile === undefined ? {} : { profile }),
|
|
129
136
|
...(traceId === undefined ? {} : { traceId }),
|
|
137
|
+
...(recording ? { events: recording.events } : {}),
|
|
130
138
|
};
|
|
131
|
-
const
|
|
139
|
+
const work = () => inRun(scope, () => {
|
|
132
140
|
const input = inputOf(definition, syncAllowedFor(key, registration.defaults));
|
|
133
141
|
const parsed = v.safeParse(input, args);
|
|
134
142
|
return parsed.success
|
|
@@ -137,7 +145,9 @@ export const entryRunner = (registration) => {
|
|
|
137
145
|
arguments: toJsonSchema(input),
|
|
138
146
|
}), app?.errors);
|
|
139
147
|
});
|
|
140
|
-
|
|
148
|
+
// The connection a session opens for this call is recorded under the same caller.
|
|
149
|
+
const result = await (caller ? withCaller(caller, work) : work());
|
|
150
|
+
if (recording) {
|
|
141
151
|
if (result.isError) {
|
|
142
152
|
const error = result.structuredContent?.error;
|
|
143
153
|
const code = error?.code && errorCodes.includes(error.code)
|
package/dist/runs/events.d.ts
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* One operation against the messenger, as both sinks see it: `--trace` renders it, a recorded run
|
|
3
3
|
* writes it as a line of JSON.
|
|
4
4
|
*
|
|
5
|
-
* ⚠ **Nothing here may carry content.** An operation name, an id, a count, a duration and an
|
|
6
|
-
* code are safe
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* ⚠ **Nothing here may carry content.** An operation name, an id, a file or folder path, a count, a duration and an
|
|
6
|
+
* error code are safe: a run record stays on the owner's machine, and keeping the ids that tell runs apart was the
|
|
7
|
+
* owner's choice (NEED-110, 2026-10-27). A chat title, a name, a message body, a phone number, a token, a URL as sent
|
|
8
|
+
* or a query string are not — not truncated and not hashed. The adapter builds `ids` and `counts` from fields it names,
|
|
9
|
+
* never by copying a request, and never from what the person typed: a typed chat is often a title. Anything that
|
|
10
|
+
* leaves the machine (a bug report) strips the ids too.
|
|
9
11
|
*/
|
|
10
12
|
export interface RequestEvent {
|
|
11
13
|
event: "request" | "response";
|
|
@@ -30,6 +32,16 @@ export interface RequestEvent {
|
|
|
30
32
|
errorCode?: string;
|
|
31
33
|
/** The messenger's own name for a refusal, when it is shaped like one (`providerErrorKey`). */
|
|
32
34
|
providerError?: string;
|
|
35
|
+
/**
|
|
36
|
+
* `operation` for one adapter call (`observed`), `wire` for one call on the network under it. A run counts wire
|
|
37
|
+
* requests when it has any, so one operation over three frames is three requests, never four.
|
|
38
|
+
*/
|
|
39
|
+
level?: "operation" | "wire";
|
|
40
|
+
transport?: "http" | "mtproto" | "max-ws" | "wa-engine";
|
|
41
|
+
/** 1 for the first try, 2 and up for a retry of the same call. */
|
|
42
|
+
attempt?: number;
|
|
43
|
+
/** How long the client waited before this attempt: a flood wait or a `Retry-After`. */
|
|
44
|
+
waitMs?: number;
|
|
33
45
|
}
|
|
34
46
|
/** Something the client noticed and went on without. A code, never the sentence. */
|
|
35
47
|
export interface WarningEvent {
|
package/dist/runs/events.js
CHANGED
|
@@ -28,6 +28,10 @@ export const renderEvent = (event) => {
|
|
|
28
28
|
parts.push([event.method, event.endpointTemplate].filter(Boolean).join(" "));
|
|
29
29
|
if (event.status !== undefined)
|
|
30
30
|
parts.push(`${event.status}`);
|
|
31
|
+
if (event.attempt !== undefined && event.attempt > 1)
|
|
32
|
+
parts.push(`attempt ${event.attempt}`);
|
|
33
|
+
if (event.waitMs !== undefined)
|
|
34
|
+
parts.push(`waited ${age(event.waitMs)}`);
|
|
31
35
|
}
|
|
32
36
|
if (event.ids)
|
|
33
37
|
for (const [name, value] of Object.entries(event.ids))
|