@flyingrobots/graft 0.3.5 → 0.5.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/ARCHITECTURE.md +386 -0
- package/CHANGELOG.md +69 -0
- package/CODE_OF_CONDUCT.md +65 -0
- package/README.md +153 -17
- package/bin/graft.js +4 -11
- package/docs/ADVANCED_GUIDE.md +49 -0
- package/docs/CLI.md +43 -0
- package/docs/GUIDE.md +321 -32
- package/docs/MCP.md +44 -0
- package/package.json +17 -4
- package/src/adapters/node-fs.ts +4 -0
- package/src/adapters/node-git.ts +47 -0
- package/src/adapters/node-process-runner.ts +27 -0
- package/src/cli/index-cmd.ts +86 -0
- package/src/cli/init.ts +808 -57
- package/src/cli/main.ts +437 -0
- package/src/contracts/capabilities.ts +341 -0
- package/src/contracts/causal-ontology.ts +622 -0
- package/src/contracts/causal-surface-next-action.ts +18 -0
- package/src/contracts/output-schemas.ts +1169 -0
- package/src/git/diff.ts +25 -21
- package/src/git/target-git-hook-bootstrap.ts +56 -0
- package/src/hooks/posttooluse-read.ts +21 -74
- package/src/hooks/pretooluse-read.ts +20 -56
- package/src/hooks/read-governor.ts +95 -0
- package/src/hooks/read-messages.ts +53 -0
- package/src/mcp/burden.ts +123 -0
- package/src/mcp/cache.ts +51 -0
- package/src/mcp/cached-file.ts +10 -8
- package/src/mcp/context.ts +67 -2
- package/src/mcp/daemon-control-plane.ts +554 -0
- package/src/mcp/daemon-job-scheduler.ts +279 -0
- package/src/mcp/daemon-repos.ts +216 -0
- package/src/mcp/daemon-server.ts +396 -0
- package/src/mcp/daemon-worker-pool.ts +310 -0
- package/src/mcp/daemon-worker-process.ts +52 -0
- package/src/mcp/metrics.ts +108 -1
- package/src/mcp/monitor-tick-job.ts +99 -0
- package/src/mcp/persisted-local-history.ts +1246 -0
- package/src/mcp/persistent-monitor-runtime.ts +549 -0
- package/src/mcp/policy.ts +84 -0
- package/src/mcp/receipt.ts +82 -12
- package/src/mcp/repo-concurrency.ts +318 -0
- package/src/mcp/repo-state.ts +777 -0
- package/src/mcp/repo-tool-job.ts +302 -0
- package/src/mcp/run-capture-config.ts +33 -0
- package/src/mcp/runtime-causal-context.ts +72 -0
- package/src/mcp/runtime-observability.ts +219 -0
- package/src/mcp/runtime-staged-target.ts +161 -0
- package/src/mcp/runtime-workspace-overlay.ts +255 -0
- package/src/mcp/semantic-transition-guidance.ts +60 -0
- package/src/mcp/semantic-transition-summary.ts +130 -0
- package/src/mcp/server.ts +704 -45
- package/src/mcp/stdio-server.ts +12 -0
- package/src/mcp/stdio.ts +2 -5
- package/src/mcp/tools/activity-view.ts +325 -0
- package/src/mcp/tools/causal-attach.ts +67 -0
- package/src/mcp/tools/causal-status.ts +58 -0
- package/src/mcp/tools/changed-since.ts +13 -11
- package/src/mcp/tools/code-find.ts +164 -0
- package/src/mcp/tools/code-refs.ts +466 -0
- package/src/mcp/tools/code-show.ts +252 -0
- package/src/mcp/tools/daemon-monitors.ts +14 -0
- package/src/mcp/tools/daemon-repos.ts +22 -0
- package/src/mcp/tools/daemon-sessions.ts +14 -0
- package/src/mcp/tools/daemon-status.ts +12 -0
- package/src/mcp/tools/doctor.ts +45 -2
- package/src/mcp/tools/explain.ts +4 -0
- package/src/mcp/tools/file-outline.ts +7 -3
- package/src/mcp/tools/git-files.ts +73 -0
- package/src/mcp/tools/graft-diff.ts +12 -4
- package/src/mcp/tools/map.ts +136 -0
- package/src/mcp/tools/monitor-pause.ts +18 -0
- package/src/mcp/tools/monitor-resume.ts +18 -0
- package/src/mcp/tools/monitor-start.ts +20 -0
- package/src/mcp/tools/monitor-stop.ts +18 -0
- package/src/mcp/tools/precision-match.ts +51 -0
- package/src/mcp/tools/precision-query.ts +127 -0
- package/src/mcp/tools/precision.ts +312 -0
- package/src/mcp/tools/run-capture.ts +126 -44
- package/src/mcp/tools/safe-read.ts +14 -12
- package/src/mcp/tools/since.ts +49 -0
- package/src/mcp/tools/state.ts +11 -3
- package/src/mcp/tools/stats.ts +5 -1
- package/src/mcp/tools/workspace-authorizations.ts +14 -0
- package/src/mcp/tools/workspace-authorize.ts +20 -0
- package/src/mcp/tools/workspace-bind.ts +25 -0
- package/src/mcp/tools/workspace-rebind.ts +25 -0
- package/src/mcp/tools/workspace-revoke.ts +18 -0
- package/src/mcp/tools/workspace-status.ts +12 -0
- package/src/mcp/warp-pool.ts +36 -0
- package/src/mcp/workspace-router.ts +984 -0
- package/src/operations/file-outline.ts +12 -2
- package/src/operations/graft-diff.ts +56 -10
- package/src/operations/safe-read.ts +27 -4
- package/src/operations/state.ts +6 -9
- package/src/parser/lang.ts +19 -3
- package/src/parser/outline.ts +191 -2
- package/src/parser/types.ts +9 -1
- package/src/policy/types.ts +4 -3
- package/src/ports/filesystem.ts +1 -0
- package/src/ports/git.ts +16 -0
- package/src/ports/process-runner.ts +22 -0
- package/src/release/security-gate.ts +102 -0
- package/src/session/tracker.ts +31 -0
- package/src/version.ts +3 -0
- package/src/warp/indexer.ts +513 -0
- package/src/warp/observers.ts +105 -0
- package/src/warp/open.ts +31 -0
- package/src/warp/plumbing.d.ts +15 -0
- package/src/warp/writer-id.ts +30 -0
package/src/mcp/server.ts
CHANGED
|
@@ -1,18 +1,48 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import * as crypto from "node:crypto";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { z } from "zod";
|
|
4
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
-
import { SessionTracker } from "../session/tracker.js";
|
|
6
|
-
import { Metrics } from "./metrics.js";
|
|
7
|
-
import { ObservationCache } from "./cache.js";
|
|
8
6
|
import { buildReceiptResult } from "./receipt.js";
|
|
9
7
|
import type { ToolHandler, ToolContext, ToolDefinition } from "./context.js";
|
|
10
|
-
import { createPathResolver } from "./context.js";
|
|
11
8
|
import type { McpToolResult } from "./receipt.js";
|
|
9
|
+
import { evaluateMcpPolicy } from "./policy.js";
|
|
12
10
|
import { nodeFs } from "../adapters/node-fs.js";
|
|
13
11
|
import { CanonicalJsonCodec } from "../adapters/canonical-json.js";
|
|
14
|
-
import {
|
|
12
|
+
import { nodeProcessRunner } from "../adapters/node-process-runner.js";
|
|
13
|
+
import type { ProcessRunner } from "../ports/process-runner.js";
|
|
14
|
+
import { nodeGit } from "../adapters/node-git.js";
|
|
15
15
|
import { RefusedResult } from "../policy/types.js";
|
|
16
|
+
import type WarpApp from "@git-stunts/git-warp";
|
|
17
|
+
import { openWarp } from "../warp/open.js";
|
|
18
|
+
import type { RunCaptureConfig } from "./run-capture-config.js";
|
|
19
|
+
import { resolveRunCaptureConfig } from "./run-capture-config.js";
|
|
20
|
+
import {
|
|
21
|
+
WorkspaceBindingRequiredError,
|
|
22
|
+
WorkspaceCapabilityDeniedError,
|
|
23
|
+
WorkspaceRouter,
|
|
24
|
+
type WorkspaceExecutionContext,
|
|
25
|
+
type WorkspaceSessionMode,
|
|
26
|
+
} from "./workspace-router.js";
|
|
27
|
+
import { DaemonControlPlane, type DaemonRuntimeDescriptor } from "./daemon-control-plane.js";
|
|
28
|
+
import { DaemonRepoOverview } from "./daemon-repos.js";
|
|
29
|
+
import { DaemonJobScheduler } from "./daemon-job-scheduler.js";
|
|
30
|
+
import { InlineDaemonWorkerPool, type DaemonWorkerPool } from "./daemon-worker-pool.js";
|
|
31
|
+
import { PersistentMonitorRuntime } from "./persistent-monitor-runtime.js";
|
|
32
|
+
import { mergeRepoConcurrencySummaryWithLiveSessions } from "./repo-concurrency.js";
|
|
33
|
+
import { OFFLOADED_DAEMON_REPO_TOOL_NAMES, type OffloadedRepoToolName } from "./repo-tool-job.js";
|
|
34
|
+
import {
|
|
35
|
+
RuntimeEventLogger,
|
|
36
|
+
classifyRuntimeFailure,
|
|
37
|
+
ensureGraftDirExcluded,
|
|
38
|
+
resolveRuntimeObservabilityState,
|
|
39
|
+
sanitizeArgKeys,
|
|
40
|
+
type RuntimeObservabilityState,
|
|
41
|
+
} from "./runtime-observability.js";
|
|
42
|
+
import { InMemoryWarpPool, type WarpPool } from "./warp-pool.js";
|
|
43
|
+
import { buildSessionWarpWriterId } from "../warp/writer-id.js";
|
|
44
|
+
import { PersistedLocalHistoryStore } from "./persisted-local-history.js";
|
|
45
|
+
import { GRAFT_VERSION } from "../version.js";
|
|
16
46
|
|
|
17
47
|
// Tool definitions — each file exports a ToolDefinition object
|
|
18
48
|
import { safeReadTool } from "./tools/safe-read.js";
|
|
@@ -26,11 +56,33 @@ import { doctorTool } from "./tools/doctor.js";
|
|
|
26
56
|
import { statsTool } from "./tools/stats.js";
|
|
27
57
|
import { explainTool } from "./tools/explain.js";
|
|
28
58
|
import { setBudgetTool } from "./tools/budget.js";
|
|
59
|
+
import { sinceTool } from "./tools/since.js";
|
|
60
|
+
import { mapTool } from "./tools/map.js";
|
|
61
|
+
import { codeShowTool } from "./tools/code-show.js";
|
|
62
|
+
import { codeFindTool } from "./tools/code-find.js";
|
|
63
|
+
import { codeRefsTool } from "./tools/code-refs.js";
|
|
64
|
+
import { daemonMonitorsTool } from "./tools/daemon-monitors.js";
|
|
65
|
+
import { daemonReposTool } from "./tools/daemon-repos.js";
|
|
66
|
+
import { daemonSessionsTool } from "./tools/daemon-sessions.js";
|
|
67
|
+
import { daemonStatusTool } from "./tools/daemon-status.js";
|
|
68
|
+
import { monitorPauseTool } from "./tools/monitor-pause.js";
|
|
69
|
+
import { monitorResumeTool } from "./tools/monitor-resume.js";
|
|
70
|
+
import { monitorStartTool } from "./tools/monitor-start.js";
|
|
71
|
+
import { monitorStopTool } from "./tools/monitor-stop.js";
|
|
72
|
+
import { activityViewTool } from "./tools/activity-view.js";
|
|
73
|
+
import { causalStatusTool } from "./tools/causal-status.js";
|
|
74
|
+
import { causalAttachTool } from "./tools/causal-attach.js";
|
|
75
|
+
import { workspaceAuthorizeTool } from "./tools/workspace-authorize.js";
|
|
76
|
+
import { workspaceAuthorizationsTool } from "./tools/workspace-authorizations.js";
|
|
77
|
+
import { workspaceBindTool } from "./tools/workspace-bind.js";
|
|
78
|
+
import { workspaceRevokeTool } from "./tools/workspace-revoke.js";
|
|
79
|
+
import { workspaceStatusTool } from "./tools/workspace-status.js";
|
|
80
|
+
import { workspaceRebindTool } from "./tools/workspace-rebind.js";
|
|
29
81
|
|
|
30
82
|
export type { McpToolResult, ToolHandler, ToolContext };
|
|
31
83
|
|
|
32
84
|
/** All registered tool definitions. Add new tools here. */
|
|
33
|
-
const TOOL_REGISTRY: readonly ToolDefinition[] = [
|
|
85
|
+
export const TOOL_REGISTRY: readonly ToolDefinition[] = [
|
|
34
86
|
safeReadTool,
|
|
35
87
|
fileOutlineTool,
|
|
36
88
|
readRangeTool,
|
|
@@ -40,69 +92,664 @@ const TOOL_REGISTRY: readonly ToolDefinition[] = [
|
|
|
40
92
|
stateSaveTool,
|
|
41
93
|
stateLoadTool,
|
|
42
94
|
doctorTool,
|
|
95
|
+
activityViewTool,
|
|
96
|
+
causalStatusTool,
|
|
97
|
+
causalAttachTool,
|
|
43
98
|
statsTool,
|
|
44
99
|
explainTool,
|
|
45
100
|
setBudgetTool,
|
|
101
|
+
sinceTool,
|
|
102
|
+
mapTool,
|
|
103
|
+
codeShowTool,
|
|
104
|
+
codeFindTool,
|
|
105
|
+
codeRefsTool,
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
export const DAEMON_TOOL_REGISTRY: readonly ToolDefinition[] = [
|
|
109
|
+
daemonReposTool,
|
|
110
|
+
daemonStatusTool,
|
|
111
|
+
daemonSessionsTool,
|
|
112
|
+
daemonMonitorsTool,
|
|
113
|
+
monitorStartTool,
|
|
114
|
+
monitorPauseTool,
|
|
115
|
+
monitorResumeTool,
|
|
116
|
+
monitorStopTool,
|
|
117
|
+
workspaceAuthorizeTool,
|
|
118
|
+
workspaceAuthorizationsTool,
|
|
119
|
+
workspaceRevokeTool,
|
|
120
|
+
workspaceBindTool,
|
|
121
|
+
workspaceStatusTool,
|
|
122
|
+
workspaceRebindTool,
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
export const ALL_TOOL_REGISTRY: readonly ToolDefinition[] = [
|
|
126
|
+
...TOOL_REGISTRY,
|
|
127
|
+
...DAEMON_TOOL_REGISTRY,
|
|
46
128
|
];
|
|
47
129
|
|
|
48
130
|
export interface GraftServer {
|
|
49
131
|
getRegisteredTools(): string[];
|
|
50
132
|
callTool(name: string, args: Record<string, unknown>): Promise<McpToolResult>;
|
|
51
133
|
injectSessionMessages(count: number): void;
|
|
134
|
+
getWorkspaceStatus(): import("./workspace-router.js").WorkspaceStatus;
|
|
135
|
+
getRuntimeCausalContext(): import("./runtime-causal-context.js").RuntimeCausalContext | null;
|
|
52
136
|
getMcpServer(): McpServer;
|
|
53
137
|
}
|
|
54
138
|
|
|
55
|
-
export
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
139
|
+
export interface CreateGraftServerOptions {
|
|
140
|
+
mode?: WorkspaceSessionMode;
|
|
141
|
+
sessionId?: string;
|
|
142
|
+
projectRoot?: string;
|
|
143
|
+
graftDir?: string;
|
|
144
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
145
|
+
runCapture?: Partial<RunCaptureConfig>;
|
|
146
|
+
runtimeObservability?: Partial<RuntimeObservabilityState>;
|
|
147
|
+
warpPool?: WarpPool;
|
|
148
|
+
daemonControlPlane?: DaemonControlPlane;
|
|
149
|
+
monitorRuntime?: PersistentMonitorRuntime;
|
|
150
|
+
daemonScheduler?: DaemonJobScheduler;
|
|
151
|
+
daemonWorkerPool?: DaemonWorkerPool;
|
|
152
|
+
daemonRuntime?: (() => DaemonRuntimeDescriptor) | undefined;
|
|
153
|
+
processRunner?: ProcessRunner;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function createGraftServer(options: CreateGraftServerOptions = {}): GraftServer {
|
|
157
|
+
const mcpServer = new McpServer({ name: "graft", version: GRAFT_VERSION });
|
|
158
|
+
const sessionId = options.sessionId ?? crypto.randomUUID();
|
|
159
|
+
const mode = options.mode ?? "repo_local";
|
|
160
|
+
const sessionWarpWriterId = mode === "daemon" ? buildSessionWarpWriterId(sessionId) : undefined;
|
|
161
|
+
const projectRoot = mode === "repo_local" ? (options.projectRoot ?? process.cwd()) : options.projectRoot;
|
|
162
|
+
const graftDir = options.graftDir
|
|
163
|
+
?? (mode === "repo_local" && projectRoot !== undefined ? path.join(projectRoot, ".graft") : undefined);
|
|
164
|
+
if (graftDir === undefined) {
|
|
165
|
+
throw new Error("daemon mode requires an explicit graftDir");
|
|
166
|
+
}
|
|
63
167
|
const codec = new CanonicalJsonCodec();
|
|
168
|
+
const runCapture = resolveRunCaptureConfig({
|
|
169
|
+
...(options.env !== undefined ? { env: options.env } : {}),
|
|
170
|
+
...(options.runCapture !== undefined ? { overrides: options.runCapture } : {}),
|
|
171
|
+
});
|
|
172
|
+
const observability = resolveRuntimeObservabilityState({
|
|
173
|
+
graftDir,
|
|
174
|
+
...(options.env !== undefined ? { env: options.env } : {}),
|
|
175
|
+
...(options.runtimeObservability !== undefined ? { overrides: options.runtimeObservability } : {}),
|
|
176
|
+
});
|
|
177
|
+
const runtimeLogger = new RuntimeEventLogger({
|
|
178
|
+
fs: nodeFs,
|
|
179
|
+
codec,
|
|
180
|
+
logPath: observability.logPath,
|
|
181
|
+
maxBytes: observability.maxBytes,
|
|
182
|
+
});
|
|
183
|
+
const warpPool = options.warpPool ?? new InMemoryWarpPool((cwd, writerId) => openWarp({ cwd, writerId }));
|
|
184
|
+
const processRunner = options.processRunner ?? nodeProcessRunner;
|
|
185
|
+
const persistedLocalHistory = new PersistedLocalHistoryStore({
|
|
186
|
+
fs: nodeFs,
|
|
187
|
+
codec,
|
|
188
|
+
graftDir,
|
|
189
|
+
});
|
|
190
|
+
const daemonScheduler = mode === "daemon" ? (options.daemonScheduler ?? new DaemonJobScheduler()) : null;
|
|
191
|
+
const daemonWorkerPool = mode === "daemon" ? (options.daemonWorkerPool ?? new InlineDaemonWorkerPool()) : null;
|
|
192
|
+
const daemonControlPlane = mode === "daemon"
|
|
193
|
+
? (options.daemonControlPlane ?? new DaemonControlPlane({
|
|
194
|
+
fs: nodeFs,
|
|
195
|
+
codec,
|
|
196
|
+
git: nodeGit,
|
|
197
|
+
graftDir,
|
|
198
|
+
}))
|
|
199
|
+
: null;
|
|
200
|
+
const monitorRuntime = mode !== "daemon" || daemonControlPlane === null || daemonScheduler === null || daemonWorkerPool === null
|
|
201
|
+
? null
|
|
202
|
+
: (options.monitorRuntime ?? new PersistentMonitorRuntime({
|
|
203
|
+
fs: nodeFs,
|
|
204
|
+
codec,
|
|
205
|
+
git: nodeGit,
|
|
206
|
+
graftDir,
|
|
207
|
+
controlPlane: daemonControlPlane,
|
|
208
|
+
scheduler: daemonScheduler,
|
|
209
|
+
workerPool: daemonWorkerPool,
|
|
210
|
+
}));
|
|
211
|
+
const daemonStartedAt = new Date().toISOString();
|
|
212
|
+
const daemonRuntime = options.daemonRuntime ?? (() => {
|
|
213
|
+
return {
|
|
214
|
+
transport: "unix_socket",
|
|
215
|
+
sameUserOnly: true,
|
|
216
|
+
socketPath: graftDir,
|
|
217
|
+
mcpPath: "/mcp",
|
|
218
|
+
healthPath: "/healthz",
|
|
219
|
+
activeWarpRepos: warpPool.size(),
|
|
220
|
+
startedAt: daemonStartedAt,
|
|
221
|
+
};
|
|
222
|
+
});
|
|
64
223
|
const handlers = new Map<string, ToolHandler>();
|
|
65
224
|
const schemas = new Map<string, z.ZodObject>();
|
|
225
|
+
const executionContextStorage = new AsyncLocalStorage<WorkspaceExecutionContext>();
|
|
226
|
+
const invocationStorage = new AsyncLocalStorage<{
|
|
227
|
+
readonly traceId: string;
|
|
228
|
+
readonly startedAtMs: number;
|
|
229
|
+
response?: { readonly receipt: import("./receipt.js").McpToolReceipt; readonly tripwireSignals: readonly string[] };
|
|
230
|
+
}>();
|
|
66
231
|
let seq = 0;
|
|
232
|
+
const workspaceRouter = new WorkspaceRouter({
|
|
233
|
+
mode,
|
|
234
|
+
fs: nodeFs,
|
|
235
|
+
git: nodeGit,
|
|
236
|
+
graftDir,
|
|
237
|
+
...(projectRoot !== undefined ? { projectRoot } : {}),
|
|
238
|
+
warpPool,
|
|
239
|
+
transportSessionId: sessionId,
|
|
240
|
+
...(sessionWarpWriterId !== undefined ? { warpWriterId: sessionWarpWriterId } : {}),
|
|
241
|
+
...(daemonControlPlane !== null ? { authorizationPolicy: daemonControlPlane } : {}),
|
|
242
|
+
...(daemonControlPlane !== null ? { sharedAttachPolicy: daemonControlPlane } : {}),
|
|
243
|
+
persistedLocalHistory,
|
|
244
|
+
});
|
|
245
|
+
const daemonRepoOverview = daemonControlPlane !== null && monitorRuntime !== null
|
|
246
|
+
? new DaemonRepoOverview({
|
|
247
|
+
controlPlane: daemonControlPlane,
|
|
248
|
+
monitorRuntime,
|
|
249
|
+
})
|
|
250
|
+
: null;
|
|
251
|
+
const runtimeReady = (async () => {
|
|
252
|
+
if (mode === "repo_local" && projectRoot !== undefined) {
|
|
253
|
+
await ensureGraftDirExcluded(projectRoot, graftDir, nodeFs);
|
|
254
|
+
}
|
|
255
|
+
await workspaceRouter.initialize();
|
|
256
|
+
await Promise.all([
|
|
257
|
+
daemonControlPlane?.initialize() ?? Promise.resolve(),
|
|
258
|
+
monitorRuntime?.initialize() ?? Promise.resolve(),
|
|
259
|
+
]);
|
|
260
|
+
})().then(() => undefined);
|
|
261
|
+
|
|
262
|
+
async function emitRuntimeEvent(event: Parameters<RuntimeEventLogger["log"]>[0]): Promise<void> {
|
|
263
|
+
try {
|
|
264
|
+
await runtimeReady;
|
|
265
|
+
await runtimeLogger.log(event);
|
|
266
|
+
} catch {
|
|
267
|
+
// Observability must never become the reason a tool call fails.
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function getActiveExecutionContext(): WorkspaceExecutionContext | null {
|
|
272
|
+
return executionContextStorage.getStore() ?? null;
|
|
273
|
+
}
|
|
67
274
|
|
|
68
|
-
function respond(tool:
|
|
275
|
+
function respond(tool: ToolDefinition["name"], data: Record<string, unknown>): McpToolResult {
|
|
276
|
+
const invocation = invocationStorage.getStore();
|
|
277
|
+
if (invocation === undefined) {
|
|
278
|
+
throw new Error("MCP respond() called outside an active invocation");
|
|
279
|
+
}
|
|
69
280
|
seq++;
|
|
70
|
-
const
|
|
71
|
-
|
|
281
|
+
const execution = getActiveExecutionContext();
|
|
282
|
+
const session = execution?.session ?? workspaceRouter.session;
|
|
283
|
+
const metrics = execution?.metrics ?? workspaceRouter.metrics;
|
|
284
|
+
const tripwires = session.checkTripwires();
|
|
285
|
+
const { result, textBytes, receipt } = buildReceiptResult(tool, data, {
|
|
286
|
+
sessionId,
|
|
287
|
+
traceId: invocation.traceId,
|
|
288
|
+
seq,
|
|
289
|
+
latencyMs: Date.now() - invocation.startedAtMs,
|
|
290
|
+
codec,
|
|
72
291
|
metrics: metrics.snapshot(),
|
|
73
|
-
tripwires
|
|
292
|
+
tripwires,
|
|
74
293
|
budget: session.getBudget(),
|
|
75
294
|
});
|
|
76
|
-
|
|
295
|
+
invocation.response = {
|
|
296
|
+
receipt,
|
|
297
|
+
tripwireSignals: tripwires.map((wire) => wire.signal),
|
|
298
|
+
};
|
|
299
|
+
metrics.recordToolResult(tool, textBytes);
|
|
77
300
|
session.recordBytesConsumed(textBytes);
|
|
78
301
|
return result;
|
|
79
302
|
}
|
|
80
303
|
|
|
81
|
-
const ctx: ToolContext = {
|
|
304
|
+
const ctx: ToolContext = {
|
|
305
|
+
get projectRoot(): string {
|
|
306
|
+
return getActiveExecutionContext()?.projectRoot ?? workspaceRouter.getProjectRoot();
|
|
307
|
+
},
|
|
308
|
+
get graftDir(): string {
|
|
309
|
+
return getActiveExecutionContext()?.graftDir ?? workspaceRouter.graftDir;
|
|
310
|
+
},
|
|
311
|
+
get graftignorePatterns(): readonly string[] {
|
|
312
|
+
return getActiveExecutionContext()?.graftignorePatterns ?? workspaceRouter.getGraftignorePatterns();
|
|
313
|
+
},
|
|
314
|
+
get session() {
|
|
315
|
+
return getActiveExecutionContext()?.session ?? workspaceRouter.session;
|
|
316
|
+
},
|
|
317
|
+
get cache() {
|
|
318
|
+
return getActiveExecutionContext()?.cache ?? workspaceRouter.cache;
|
|
319
|
+
},
|
|
320
|
+
get metrics() {
|
|
321
|
+
return getActiveExecutionContext()?.metrics ?? workspaceRouter.metrics;
|
|
322
|
+
},
|
|
323
|
+
respond,
|
|
324
|
+
resolvePath(relative: string): string {
|
|
325
|
+
return getActiveExecutionContext()?.resolvePath(relative) ?? workspaceRouter.getPathResolver()(relative);
|
|
326
|
+
},
|
|
327
|
+
fs: nodeFs,
|
|
328
|
+
codec,
|
|
329
|
+
process: processRunner,
|
|
330
|
+
git: nodeGit,
|
|
331
|
+
runCapture,
|
|
332
|
+
observability,
|
|
333
|
+
getWarp(): Promise<WarpApp> {
|
|
334
|
+
return getActiveExecutionContext()?.getWarp() ?? workspaceRouter.getWarp();
|
|
335
|
+
},
|
|
336
|
+
getRepoState() {
|
|
337
|
+
return getActiveExecutionContext()?.repoState.getState() ?? workspaceRouter.getRepoState();
|
|
338
|
+
},
|
|
339
|
+
getCausalContext() {
|
|
340
|
+
return getActiveExecutionContext()?.getCausalContext() ?? workspaceRouter.captureExecutionContext().getCausalContext();
|
|
341
|
+
},
|
|
342
|
+
getWorkspaceOverlayFooting() {
|
|
343
|
+
return workspaceRouter.getWorkspaceOverlayFooting();
|
|
344
|
+
},
|
|
345
|
+
getPersistedLocalHistorySummary() {
|
|
346
|
+
return workspaceRouter.getPersistedLocalHistorySummary();
|
|
347
|
+
},
|
|
348
|
+
getPersistedLocalActivityWindow(limit) {
|
|
349
|
+
return workspaceRouter.getPersistedLocalActivityWindow(limit);
|
|
350
|
+
},
|
|
351
|
+
getRepoConcurrencySummary() {
|
|
352
|
+
return (async () => {
|
|
353
|
+
const summary = await workspaceRouter.getRepoConcurrencySummary();
|
|
354
|
+
if (summary === null || daemonControlPlane === null) {
|
|
355
|
+
return summary;
|
|
356
|
+
}
|
|
357
|
+
const status = workspaceRouter.getStatus();
|
|
358
|
+
if (status.bindState !== "bound" || status.repoId === null || status.worktreeId === null) {
|
|
359
|
+
return summary;
|
|
360
|
+
}
|
|
361
|
+
const causalContext = workspaceRouter.captureExecutionContext().getCausalContext();
|
|
362
|
+
return mergeRepoConcurrencySummaryWithLiveSessions({
|
|
363
|
+
currentSummary: summary,
|
|
364
|
+
currentRepoId: status.repoId,
|
|
365
|
+
currentWorktreeId: status.worktreeId,
|
|
366
|
+
currentCheckoutEpochId: causalContext.checkoutEpochId,
|
|
367
|
+
sessions: daemonControlPlane.listSessions(),
|
|
368
|
+
});
|
|
369
|
+
})();
|
|
370
|
+
},
|
|
371
|
+
declareCausalAttach(request) {
|
|
372
|
+
return workspaceRouter.declareAttach(request);
|
|
373
|
+
},
|
|
374
|
+
getWorkspaceStatus() {
|
|
375
|
+
return getActiveExecutionContext()?.status ?? workspaceRouter.getStatus();
|
|
376
|
+
},
|
|
377
|
+
bindWorkspace(request, actionName) {
|
|
378
|
+
return workspaceRouter.bind(request, actionName);
|
|
379
|
+
},
|
|
380
|
+
rebindWorkspace(request, actionName) {
|
|
381
|
+
return workspaceRouter.rebind(request, actionName);
|
|
382
|
+
},
|
|
383
|
+
getDaemonStatus() {
|
|
384
|
+
if (daemonControlPlane === null) {
|
|
385
|
+
throw new Error("daemon_status is only available in daemon mode");
|
|
386
|
+
}
|
|
387
|
+
if (daemonScheduler === null) {
|
|
388
|
+
throw new Error("daemon_status is only available in daemon mode");
|
|
389
|
+
}
|
|
390
|
+
if (daemonWorkerPool === null) {
|
|
391
|
+
throw new Error("daemon_status is only available in daemon mode");
|
|
392
|
+
}
|
|
393
|
+
return Promise.resolve(daemonControlPlane.getStatus(
|
|
394
|
+
daemonRuntime(),
|
|
395
|
+
monitorRuntime?.getCounts(),
|
|
396
|
+
daemonScheduler.getCounts(),
|
|
397
|
+
daemonWorkerPool.getCounts(),
|
|
398
|
+
));
|
|
399
|
+
},
|
|
400
|
+
listDaemonRepos(filter) {
|
|
401
|
+
if (daemonRepoOverview === null) {
|
|
402
|
+
throw new Error("daemon_repos is only available in daemon mode");
|
|
403
|
+
}
|
|
404
|
+
return daemonRepoOverview.list(filter);
|
|
405
|
+
},
|
|
406
|
+
listDaemonSessions() {
|
|
407
|
+
if (daemonControlPlane === null) {
|
|
408
|
+
throw new Error("daemon_sessions is only available in daemon mode");
|
|
409
|
+
}
|
|
410
|
+
return Promise.resolve(daemonControlPlane.listSessions());
|
|
411
|
+
},
|
|
412
|
+
listDaemonMonitors() {
|
|
413
|
+
if (monitorRuntime === null) {
|
|
414
|
+
throw new Error("daemon_monitors is only available in daemon mode");
|
|
415
|
+
}
|
|
416
|
+
return monitorRuntime.listStatuses();
|
|
417
|
+
},
|
|
418
|
+
startMonitor(request) {
|
|
419
|
+
if (monitorRuntime === null) {
|
|
420
|
+
throw new Error("monitor_start is only available in daemon mode");
|
|
421
|
+
}
|
|
422
|
+
return monitorRuntime.startMonitor(request);
|
|
423
|
+
},
|
|
424
|
+
pauseMonitor(request) {
|
|
425
|
+
if (monitorRuntime === null) {
|
|
426
|
+
throw new Error("monitor_pause is only available in daemon mode");
|
|
427
|
+
}
|
|
428
|
+
return monitorRuntime.pauseMonitor(request);
|
|
429
|
+
},
|
|
430
|
+
resumeMonitor(request) {
|
|
431
|
+
if (monitorRuntime === null) {
|
|
432
|
+
throw new Error("monitor_resume is only available in daemon mode");
|
|
433
|
+
}
|
|
434
|
+
return monitorRuntime.resumeMonitor(request);
|
|
435
|
+
},
|
|
436
|
+
stopMonitor(request) {
|
|
437
|
+
if (monitorRuntime === null) {
|
|
438
|
+
throw new Error("monitor_stop is only available in daemon mode");
|
|
439
|
+
}
|
|
440
|
+
return monitorRuntime.stopMonitor(request);
|
|
441
|
+
},
|
|
442
|
+
listWorkspaceAuthorizations() {
|
|
443
|
+
if (daemonControlPlane === null) {
|
|
444
|
+
throw new Error("workspace_authorizations is only available in daemon mode");
|
|
445
|
+
}
|
|
446
|
+
return daemonControlPlane.listAuthorizedWorkspaces();
|
|
447
|
+
},
|
|
448
|
+
authorizeWorkspace(request) {
|
|
449
|
+
if (daemonControlPlane === null) {
|
|
450
|
+
throw new Error("workspace_authorize is only available in daemon mode");
|
|
451
|
+
}
|
|
452
|
+
return daemonControlPlane.authorizeWorkspace(request);
|
|
453
|
+
},
|
|
454
|
+
revokeWorkspace(request) {
|
|
455
|
+
if (daemonControlPlane === null) {
|
|
456
|
+
throw new Error("workspace_revoke is only available in daemon mode");
|
|
457
|
+
}
|
|
458
|
+
return daemonControlPlane.revokeWorkspace(request);
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const daemonAlwaysAvailableTools = new Set<string>([
|
|
463
|
+
"daemon_repos",
|
|
464
|
+
"daemon_status",
|
|
465
|
+
"daemon_sessions",
|
|
466
|
+
"daemon_monitors",
|
|
467
|
+
"monitor_start",
|
|
468
|
+
"monitor_pause",
|
|
469
|
+
"monitor_resume",
|
|
470
|
+
"monitor_stop",
|
|
471
|
+
"workspace_authorize",
|
|
472
|
+
"workspace_authorizations",
|
|
473
|
+
"workspace_revoke",
|
|
474
|
+
"workspace_bind",
|
|
475
|
+
"workspace_status",
|
|
476
|
+
"causal_status",
|
|
477
|
+
"workspace_rebind",
|
|
478
|
+
"explain",
|
|
479
|
+
]);
|
|
480
|
+
|
|
481
|
+
const repoStateOptionalTools = new Set<string>([
|
|
482
|
+
"daemon_repos",
|
|
483
|
+
"daemon_status",
|
|
484
|
+
"daemon_sessions",
|
|
485
|
+
"daemon_monitors",
|
|
486
|
+
"monitor_start",
|
|
487
|
+
"monitor_pause",
|
|
488
|
+
"monitor_resume",
|
|
489
|
+
"monitor_stop",
|
|
490
|
+
"workspace_authorize",
|
|
491
|
+
"workspace_authorizations",
|
|
492
|
+
"workspace_revoke",
|
|
493
|
+
"workspace_bind",
|
|
494
|
+
"workspace_status",
|
|
495
|
+
"workspace_rebind",
|
|
496
|
+
"explain",
|
|
497
|
+
]);
|
|
498
|
+
|
|
499
|
+
function enforceDaemonToolAccess(name: string): void {
|
|
500
|
+
if (mode !== "daemon") return;
|
|
501
|
+
if (daemonAlwaysAvailableTools.has(name)) {
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (!workspaceRouter.isBound()) {
|
|
505
|
+
throw new WorkspaceBindingRequiredError(name);
|
|
506
|
+
}
|
|
507
|
+
if (name === "run_capture" && workspaceRouter.getStatus().capabilityProfile?.runCapture !== true) {
|
|
508
|
+
throw new WorkspaceCapabilityDeniedError(name);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const daemonScheduledRepoTools = new Set<string>([
|
|
513
|
+
"safe_read",
|
|
514
|
+
"file_outline",
|
|
515
|
+
"changed_since",
|
|
516
|
+
"graft_diff",
|
|
517
|
+
"graft_since",
|
|
518
|
+
"graft_map",
|
|
519
|
+
"code_show",
|
|
520
|
+
"code_find",
|
|
521
|
+
"code_refs",
|
|
522
|
+
]);
|
|
523
|
+
const daemonOffloadedRepoTools = new Set<string>(OFFLOADED_DAEMON_REPO_TOOL_NAMES);
|
|
524
|
+
const attributedReadTools = new Set<string>([
|
|
525
|
+
"safe_read",
|
|
526
|
+
"file_outline",
|
|
527
|
+
"read_range",
|
|
528
|
+
]);
|
|
529
|
+
|
|
530
|
+
function isOffloadedRepoTool(name: string): name is OffloadedRepoToolName {
|
|
531
|
+
return daemonOffloadedRepoTools.has(name);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function resolveDaemonOffloadedRepoTool(
|
|
535
|
+
name: string,
|
|
536
|
+
parsed: Record<string, unknown>,
|
|
537
|
+
dirty: boolean,
|
|
538
|
+
): OffloadedRepoToolName | null {
|
|
539
|
+
if (name === "code_find") {
|
|
540
|
+
return dirty ? "code_find_live" : null;
|
|
541
|
+
}
|
|
542
|
+
if (name === "code_show") {
|
|
543
|
+
return parsed["ref"] === undefined ? "code_show_live" : null;
|
|
544
|
+
}
|
|
545
|
+
return isOffloadedRepoTool(name) ? name : null;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function parseToolPayload(result: McpToolResult): Record<string, unknown> | null {
|
|
549
|
+
const textBlock = result.content.find((entry) => entry.type === "text");
|
|
550
|
+
if (textBlock === undefined) {
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
return JSON.parse(textBlock.text) as Record<string, unknown>;
|
|
555
|
+
} catch {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async function invokeTool(
|
|
561
|
+
name: string,
|
|
562
|
+
handler: ToolHandler,
|
|
563
|
+
args: Record<string, unknown>,
|
|
564
|
+
schema?: z.ZodObject,
|
|
565
|
+
): Promise<McpToolResult> {
|
|
566
|
+
await runtimeReady;
|
|
567
|
+
workspaceRouter.session.recordMessage();
|
|
568
|
+
workspaceRouter.session.recordToolCall(name);
|
|
569
|
+
const traceId = crypto.randomUUID();
|
|
570
|
+
const startedAtMs = Date.now();
|
|
571
|
+
const argKeys = sanitizeArgKeys(args);
|
|
572
|
+
|
|
573
|
+
if (observability.enabled) {
|
|
574
|
+
await emitRuntimeEvent({
|
|
575
|
+
event: "tool_call_started",
|
|
576
|
+
sessionId,
|
|
577
|
+
traceId,
|
|
578
|
+
tool: name,
|
|
579
|
+
argKeys,
|
|
580
|
+
sessionDepth: workspaceRouter.session.getSessionDepth(),
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const invocation = { traceId, startedAtMs } as {
|
|
585
|
+
readonly traceId: string;
|
|
586
|
+
readonly startedAtMs: number;
|
|
587
|
+
response?: { readonly receipt: import("./receipt.js").McpToolReceipt; readonly tripwireSignals: readonly string[] };
|
|
588
|
+
};
|
|
589
|
+
let execution: WorkspaceExecutionContext | null = null;
|
|
590
|
+
|
|
591
|
+
try {
|
|
592
|
+
const parsed: Record<string, unknown> = schema !== undefined ? schema.parse(args) : args;
|
|
593
|
+
enforceDaemonToolAccess(name);
|
|
594
|
+
|
|
595
|
+
execution = daemonScheduler !== null
|
|
596
|
+
&& workspaceRouter.isBound()
|
|
597
|
+
&& daemonScheduledRepoTools.has(name)
|
|
598
|
+
? workspaceRouter.captureExecutionContext()
|
|
599
|
+
: null;
|
|
600
|
+
|
|
601
|
+
const executeHandler = async (): Promise<McpToolResult> => {
|
|
602
|
+
return invocationStorage.run(invocation, async () => {
|
|
603
|
+
if (execution !== null) {
|
|
604
|
+
const activeExecution = execution;
|
|
605
|
+
return executionContextStorage.run(activeExecution, async () => {
|
|
606
|
+
if (!repoStateOptionalTools.has(name)) {
|
|
607
|
+
await activeExecution.repoState.observe();
|
|
608
|
+
}
|
|
609
|
+
return handler(parsed);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
if (workspaceRouter.isBound() && !repoStateOptionalTools.has(name)) {
|
|
613
|
+
await workspaceRouter.observeRepoState();
|
|
614
|
+
}
|
|
615
|
+
return handler(parsed);
|
|
616
|
+
});
|
|
617
|
+
};
|
|
618
|
+
|
|
619
|
+
const result = execution !== null && daemonScheduler !== null
|
|
620
|
+
? await daemonScheduler.enqueue({
|
|
621
|
+
sessionId,
|
|
622
|
+
sliceId: execution.sliceId,
|
|
623
|
+
repoId: execution.repoId,
|
|
624
|
+
worktreeId: execution.worktreeId,
|
|
625
|
+
tool: name,
|
|
626
|
+
kind: "repo_tool",
|
|
627
|
+
priority: "interactive",
|
|
628
|
+
writerId: execution.warpWriterId,
|
|
629
|
+
}, async () => {
|
|
630
|
+
const activeExecution = execution;
|
|
631
|
+
if (activeExecution !== null && daemonWorkerPool !== null) {
|
|
632
|
+
await activeExecution.repoState.observe();
|
|
633
|
+
const offloadedTool = resolveDaemonOffloadedRepoTool(
|
|
634
|
+
name,
|
|
635
|
+
parsed,
|
|
636
|
+
activeExecution.repoState.getState().dirty,
|
|
637
|
+
);
|
|
638
|
+
if (offloadedTool === null) {
|
|
639
|
+
return executeHandler();
|
|
640
|
+
}
|
|
641
|
+
const cacheSnapshots = typeof parsed["path"] === "string"
|
|
642
|
+
? (() => {
|
|
643
|
+
const filePath = activeExecution.resolvePath(parsed["path"]);
|
|
644
|
+
const snapshot = activeExecution.cache.snapshotEntry(filePath);
|
|
645
|
+
return snapshot === null ? {} : { [filePath]: snapshot };
|
|
646
|
+
})()
|
|
647
|
+
: undefined;
|
|
648
|
+
const workerResult = await daemonWorkerPool.runRepoTool({
|
|
649
|
+
sessionId,
|
|
650
|
+
workspaceSliceId: activeExecution.sliceId,
|
|
651
|
+
traceId,
|
|
652
|
+
seq: ++seq,
|
|
653
|
+
startedAtMs,
|
|
654
|
+
tool: offloadedTool,
|
|
655
|
+
args: parsed,
|
|
656
|
+
projectRoot: activeExecution.projectRoot,
|
|
657
|
+
graftDir: activeExecution.graftDir,
|
|
658
|
+
graftignorePatterns: activeExecution.graftignorePatterns,
|
|
659
|
+
repoId: activeExecution.repoId,
|
|
660
|
+
worktreeId: activeExecution.worktreeId,
|
|
661
|
+
gitCommonDir: activeExecution.gitCommonDir,
|
|
662
|
+
writerId: activeExecution.warpWriterId,
|
|
663
|
+
capabilityProfile: activeExecution.capabilityProfile,
|
|
664
|
+
repoState: activeExecution.repoState.getState(),
|
|
665
|
+
sessionSnapshot: activeExecution.session.snapshot(),
|
|
666
|
+
metricsSnapshot: activeExecution.metrics.snapshot(),
|
|
667
|
+
...(cacheSnapshots !== undefined ? { cacheSnapshots } : {}),
|
|
668
|
+
});
|
|
669
|
+
for (const update of workerResult.cacheUpdates) {
|
|
670
|
+
activeExecution.cache.applySnapshot(update.path, update.observation);
|
|
671
|
+
}
|
|
672
|
+
activeExecution.metrics.applyDelta(workerResult.metricsDelta);
|
|
673
|
+
activeExecution.metrics.recordToolResult(name as ToolDefinition["name"], workerResult.textBytes);
|
|
674
|
+
activeExecution.session.recordBytesConsumed(workerResult.textBytes);
|
|
675
|
+
invocation.response = {
|
|
676
|
+
receipt: workerResult.receipt,
|
|
677
|
+
tripwireSignals: workerResult.tripwireSignals,
|
|
678
|
+
};
|
|
679
|
+
return workerResult.result;
|
|
680
|
+
}
|
|
681
|
+
return executeHandler();
|
|
682
|
+
})
|
|
683
|
+
: await executeHandler();
|
|
684
|
+
|
|
685
|
+
if (observability.enabled && invocation.response !== undefined) {
|
|
686
|
+
await emitRuntimeEvent({
|
|
687
|
+
event: "tool_call_completed",
|
|
688
|
+
sessionId,
|
|
689
|
+
traceId,
|
|
690
|
+
seq: invocation.response.receipt.seq,
|
|
691
|
+
tool: name,
|
|
692
|
+
latencyMs: invocation.response.receipt.latencyMs,
|
|
693
|
+
projection: invocation.response.receipt.projection,
|
|
694
|
+
reason: invocation.response.receipt.reason,
|
|
695
|
+
burdenKind: invocation.response.receipt.burden.kind,
|
|
696
|
+
nonReadBurden: invocation.response.receipt.burden.nonRead,
|
|
697
|
+
returnedBytes: invocation.response.receipt.returnedBytes,
|
|
698
|
+
fileBytes: invocation.response.receipt.fileBytes,
|
|
699
|
+
sessionDepth: execution?.session.getSessionDepth() ?? workspaceRouter.session.getSessionDepth(),
|
|
700
|
+
tripwireSignals: invocation.response.tripwireSignals,
|
|
701
|
+
metrics: invocation.response.receipt.cumulative,
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (workspaceRouter.isBound() && attributedReadTools.has(name)) {
|
|
706
|
+
const payload = parseToolPayload(result);
|
|
707
|
+
if (payload !== null) {
|
|
708
|
+
await workspaceRouter.noteReadObservation(
|
|
709
|
+
name as "safe_read" | "file_outline" | "read_range",
|
|
710
|
+
parsed,
|
|
711
|
+
payload,
|
|
712
|
+
execution,
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
82
716
|
|
|
83
|
-
|
|
84
|
-
|
|
717
|
+
return result;
|
|
718
|
+
} catch (error) {
|
|
719
|
+
if (observability.enabled) {
|
|
720
|
+
const failure = classifyRuntimeFailure(error);
|
|
721
|
+
await emitRuntimeEvent({
|
|
722
|
+
event: "tool_call_failed",
|
|
723
|
+
sessionId,
|
|
724
|
+
traceId,
|
|
725
|
+
tool: name,
|
|
726
|
+
latencyMs: Date.now() - startedAtMs,
|
|
727
|
+
sessionDepth: execution?.session.getSessionDepth() ?? workspaceRouter.session.getSessionDepth(),
|
|
728
|
+
argKeys,
|
|
729
|
+
errorKind: failure.kind,
|
|
730
|
+
errorName: failure.name,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
throw error;
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function wrapWithPolicyCheck(toolName: ToolDefinition["name"], inner: ToolHandler): ToolHandler {
|
|
738
|
+
return async (args: Record<string, unknown>) => {
|
|
85
739
|
const rawPath = args["path"] as string | undefined;
|
|
86
740
|
if (rawPath === undefined) return inner(args);
|
|
87
741
|
const filePath = ctx.resolvePath(rawPath);
|
|
88
742
|
let content: string;
|
|
89
743
|
try {
|
|
90
|
-
content = ctx.fs.
|
|
744
|
+
content = await ctx.fs.readFile(filePath, "utf-8");
|
|
91
745
|
} catch {
|
|
92
746
|
// File unreadable — let the inner handler deal with the error
|
|
93
747
|
return inner(args);
|
|
94
748
|
}
|
|
95
749
|
const actual = { lines: content.split("\n").length, bytes: Buffer.byteLength(content) };
|
|
96
|
-
const
|
|
97
|
-
const policy = evaluatePolicy(
|
|
98
|
-
{ path: filePath, lines: actual.lines, bytes: actual.bytes },
|
|
99
|
-
{
|
|
100
|
-
sessionDepth: session.getSessionDepth(),
|
|
101
|
-
budgetRemaining: budget?.remaining,
|
|
102
|
-
},
|
|
103
|
-
);
|
|
750
|
+
const policy = evaluateMcpPolicy(ctx, filePath, actual);
|
|
104
751
|
if (policy instanceof RefusedResult) {
|
|
105
|
-
metrics.recordRefusal();
|
|
752
|
+
ctx.metrics.recordRefusal();
|
|
106
753
|
return respond(toolName, {
|
|
107
754
|
path: filePath,
|
|
108
755
|
projection: "refused",
|
|
@@ -116,7 +763,9 @@ export function createGraftServer(): GraftServer {
|
|
|
116
763
|
};
|
|
117
764
|
}
|
|
118
765
|
|
|
119
|
-
|
|
766
|
+
const activeToolRegistry = mode === "daemon" ? ALL_TOOL_REGISTRY : TOOL_REGISTRY;
|
|
767
|
+
|
|
768
|
+
for (const def of activeToolRegistry) {
|
|
120
769
|
const rawHandler = def.createHandler(ctx);
|
|
121
770
|
const handler = def.policyCheck === true ? wrapWithPolicyCheck(def.name, rawHandler) : rawHandler;
|
|
122
771
|
handlers.set(def.name, handler);
|
|
@@ -125,20 +774,24 @@ export function createGraftServer(): GraftServer {
|
|
|
125
774
|
const zodSchema = z.object(def.schema).strict();
|
|
126
775
|
schemas.set(def.name, zodSchema);
|
|
127
776
|
mcpServer.registerTool(def.name, { description: def.description, inputSchema: def.schema }, async (args) => {
|
|
128
|
-
|
|
129
|
-
session.recordToolCall(def.name);
|
|
130
|
-
const parsed: Record<string, unknown> = zodSchema.parse(args);
|
|
131
|
-
return handler(parsed);
|
|
777
|
+
return invokeTool(def.name, handler, args, zodSchema);
|
|
132
778
|
});
|
|
133
779
|
} else {
|
|
134
780
|
mcpServer.registerTool(def.name, { description: def.description }, async () => {
|
|
135
|
-
|
|
136
|
-
session.recordToolCall(def.name);
|
|
137
|
-
return handler({});
|
|
781
|
+
return invokeTool(def.name, handler, {});
|
|
138
782
|
});
|
|
139
783
|
}
|
|
140
784
|
}
|
|
141
785
|
|
|
786
|
+
if (observability.enabled) {
|
|
787
|
+
void emitRuntimeEvent({
|
|
788
|
+
event: "session_started",
|
|
789
|
+
sessionId,
|
|
790
|
+
logPath: observability.logPath,
|
|
791
|
+
logPolicy: observability.logPolicy,
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
142
795
|
return {
|
|
143
796
|
getRegisteredTools(): string[] {
|
|
144
797
|
return [...handlers.keys()];
|
|
@@ -148,16 +801,22 @@ export function createGraftServer(): GraftServer {
|
|
|
148
801
|
if (handler === undefined) {
|
|
149
802
|
throw new Error(`Unknown tool: ${name}`);
|
|
150
803
|
}
|
|
151
|
-
session.recordMessage();
|
|
152
|
-
session.recordToolCall(name);
|
|
153
804
|
const schema = schemas.get(name);
|
|
154
|
-
|
|
155
|
-
return handler(parsed);
|
|
805
|
+
return invokeTool(name, handler, args, schema);
|
|
156
806
|
},
|
|
157
807
|
injectSessionMessages(count: number): void {
|
|
158
808
|
for (let i = 0; i < count; i++) {
|
|
159
|
-
session.recordMessage();
|
|
809
|
+
workspaceRouter.session.recordMessage();
|
|
810
|
+
}
|
|
811
|
+
},
|
|
812
|
+
getWorkspaceStatus() {
|
|
813
|
+
return workspaceRouter.getStatus();
|
|
814
|
+
},
|
|
815
|
+
getRuntimeCausalContext() {
|
|
816
|
+
if (workspaceRouter.getStatus().bindState !== "bound") {
|
|
817
|
+
return null;
|
|
160
818
|
}
|
|
819
|
+
return workspaceRouter.captureExecutionContext().getCausalContext();
|
|
161
820
|
},
|
|
162
821
|
getMcpServer(): McpServer {
|
|
163
822
|
return mcpServer;
|