@easonwumac/computer-linker 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +230 -0
  2. package/LICENSE +21 -0
  3. package/README.md +539 -0
  4. package/SECURITY.md +48 -0
  5. package/dist/api.d.ts +2 -0
  6. package/dist/api.js +360 -0
  7. package/dist/audit.d.ts +70 -0
  8. package/dist/audit.js +102 -0
  9. package/dist/capabilities.d.ts +98 -0
  10. package/dist/capabilities.js +718 -0
  11. package/dist/capability-policy.d.ts +22 -0
  12. package/dist/capability-policy.js +103 -0
  13. package/dist/chatgpt.d.ts +167 -0
  14. package/dist/chatgpt.js +561 -0
  15. package/dist/cli.d.ts +2 -0
  16. package/dist/cli.js +4621 -0
  17. package/dist/client-smoke.d.ts +44 -0
  18. package/dist/client-smoke.js +639 -0
  19. package/dist/client.d.ts +217 -0
  20. package/dist/client.js +357 -0
  21. package/dist/codex-runs.d.ts +35 -0
  22. package/dist/codex-runs.js +66 -0
  23. package/dist/computer-contract.d.ts +33 -0
  24. package/dist/computer-contract.js +384 -0
  25. package/dist/computer-operation-registry.d.ts +45 -0
  26. package/dist/computer-operation-registry.js +179 -0
  27. package/dist/config-diagnostics.d.ts +11 -0
  28. package/dist/config-diagnostics.js +185 -0
  29. package/dist/config.d.ts +10 -0
  30. package/dist/config.js +69 -0
  31. package/dist/history-insights.d.ts +132 -0
  32. package/dist/history-insights.js +457 -0
  33. package/dist/http-auth.d.ts +3 -0
  34. package/dist/http-auth.js +15 -0
  35. package/dist/mcp-surface.d.ts +5 -0
  36. package/dist/mcp-surface.js +25 -0
  37. package/dist/oauth-provider.d.ts +52 -0
  38. package/dist/oauth-provider.js +325 -0
  39. package/dist/package-metadata.d.ts +7 -0
  40. package/dist/package-metadata.js +24 -0
  41. package/dist/permissions.d.ts +43 -0
  42. package/dist/permissions.js +150 -0
  43. package/dist/platform-shell.d.ts +28 -0
  44. package/dist/platform-shell.js +124 -0
  45. package/dist/processes.d.ts +50 -0
  46. package/dist/processes.js +178 -0
  47. package/dist/profile.d.ts +159 -0
  48. package/dist/profile.js +416 -0
  49. package/dist/screenshot.d.ts +47 -0
  50. package/dist/screenshot.js +302 -0
  51. package/dist/search.d.ts +34 -0
  52. package/dist/search.js +340 -0
  53. package/dist/security.d.ts +10 -0
  54. package/dist/security.js +108 -0
  55. package/dist/sensitive-files.d.ts +4 -0
  56. package/dist/sensitive-files.js +96 -0
  57. package/dist/server.d.ts +9 -0
  58. package/dist/server.js +713 -0
  59. package/dist/service.d.ts +125 -0
  60. package/dist/service.js +486 -0
  61. package/dist/sessions.d.ts +26 -0
  62. package/dist/sessions.js +34 -0
  63. package/dist/tunnels.d.ts +161 -0
  64. package/dist/tunnels.js +1243 -0
  65. package/dist/workspace-operations.d.ts +170 -0
  66. package/dist/workspace-operations.js +3219 -0
  67. package/dist/workspaces.d.ts +61 -0
  68. package/dist/workspaces.js +353 -0
  69. package/docs/agent-instructions.md +65 -0
  70. package/docs/alpha-evidence.example.json +54 -0
  71. package/docs/api-compatibility.md +56 -0
  72. package/docs/architecture.md +561 -0
  73. package/docs/chatgpt-setup.md +397 -0
  74. package/docs/client-recipes.md +98 -0
  75. package/docs/client-sdk.md +163 -0
  76. package/docs/computer-operation-v1.schema.json +143 -0
  77. package/docs/manual-test-plan.md +322 -0
  78. package/docs/product-spec.md +911 -0
  79. package/docs/release-checklist.md +285 -0
  80. package/docs/service-mode.md +99 -0
  81. package/examples/minimal-mcp-client.mjs +114 -0
  82. package/package.json +87 -0
@@ -0,0 +1,33 @@
1
+ import type { AuditEventInput } from "./audit.js";
2
+ import { type ComputerOperationEnvelope } from "./computer-operation-registry.js";
3
+ import { type TunnelProcessSnapshot } from "./tunnels.js";
4
+ import { type WorkspaceOperationInput } from "./workspace-operations.js";
5
+ export interface McpClientSetupOptions {
6
+ tunnels?: TunnelProcessSnapshot[];
7
+ includeSecrets?: boolean;
8
+ }
9
+ export type ComputerOperationErrorCode = "invalid_request" | "unknown_scope" | "unknown_operation" | "permission_denied" | "path_out_of_scope" | "unsupported_platform" | "provider_unavailable" | "timeout" | "process_not_found" | "os_permission_required" | "execution_failed";
10
+ export interface ComputerOperationError {
11
+ code: ComputerOperationErrorCode;
12
+ message: string;
13
+ retryable: boolean;
14
+ details: Record<string, unknown>;
15
+ }
16
+ export interface ComputerOperationResult<T = unknown> {
17
+ ok: boolean;
18
+ operationId: string;
19
+ scope: string;
20
+ op: string;
21
+ startedAt: string;
22
+ durationMs: number;
23
+ data?: T;
24
+ error?: ComputerOperationError;
25
+ warnings: string[];
26
+ }
27
+ export declare function getComputerInfo(): unknown;
28
+ export declare function getMcpClientSetup(options?: McpClientSetupOptions): unknown;
29
+ export declare function runComputerOperation(envelope: ComputerOperationEnvelope): Promise<ComputerOperationResult>;
30
+ export declare function getOperationHistory(input: Record<string, unknown>): unknown;
31
+ export declare function normalizeComputerOperationInput(envelope: ComputerOperationEnvelope, workspaceOperation?: string): WorkspaceOperationInput;
32
+ export declare function computerOperationAuditFields(envelope: ComputerOperationEnvelope): Promise<Partial<AuditEventInput>>;
33
+ export declare function computerOperationName(op: string): string;
@@ -0,0 +1,384 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { platform, release, arch, type } from "node:os";
3
+ import { basename } from "node:path";
4
+ import { readAuditEvents } from "./audit.js";
5
+ import { getLocalPortCapabilities } from "./capabilities.js";
6
+ import { workspaceCapabilityPolicy } from "./capability-policy.js";
7
+ import { computerOperationContract, computerOperationMap, publicComputerOperationRegistry, } from "./computer-operation-registry.js";
8
+ import { loadConfig } from "./config.js";
9
+ import { historyInsight } from "./history-insights.js";
10
+ import { compatibilityMcpTools, exposedMcpTools, genericMcpTools, mcpToolSurface } from "./mcp-surface.js";
11
+ import { workspaceLinkerVersion } from "./package-metadata.js";
12
+ import { PermissionDeniedError } from "./permissions.js";
13
+ import { connectionProfile } from "./profile.js";
14
+ import { screenshotCapability } from "./screenshot.js";
15
+ import { listTunnelProcesses } from "./tunnels.js";
16
+ import { WorkspaceRegistry } from "./workspaces.js";
17
+ import { allowedWorkspaceOperations, normalizeWorkspaceOperationInput, runWorkspaceOperation, workspaceOperationAuditFields, } from "./workspace-operations.js";
18
+ const historyViewMap = {
19
+ "history.last": "last",
20
+ "history.timeline": "timeline",
21
+ "history.sessions": "sessions",
22
+ "history.connections": "connections",
23
+ "history.failed_replay": "failed_replay",
24
+ "history.debug_bundle": "debug_bundle",
25
+ };
26
+ const genericAgentInstructions = [
27
+ "You are connected to Computer Linker, a local MCP server for this computer.",
28
+ "First call get_computer_info to inspect available scopes, permissions, and safety boundaries.",
29
+ "Call computer_operation with dotted ops from computerOperationRegistry and the stable envelope {scope, op, target, input, options}.",
30
+ "Stay inside configured scopes. Prefer file.search, file.read, code.context, and get_operation_history before write.",
31
+ "Use write, shell, command, or codex operations only when the reported permissions allow them.",
32
+ "Do not call workspace_operation, read, ls, grep, glob, or create_file unless the server explicitly exposes compatibility tools.",
33
+ "If tunnel or connection behavior is unclear, inspect get_operation_history before changing anything.",
34
+ ];
35
+ export function getComputerInfo() {
36
+ const config = loadConfig();
37
+ const registry = new WorkspaceRegistry(config);
38
+ const capabilities = getLocalPortCapabilities();
39
+ const activeMcpToolSurface = mcpToolSurface();
40
+ return {
41
+ kind: "computer-linker-computer-info",
42
+ schemaVersion: 1,
43
+ machineId: config.machineId,
44
+ machineName: config.machineName,
45
+ platform: {
46
+ os: platform(),
47
+ name: type(),
48
+ arch: arch(),
49
+ release: release(),
50
+ shell: process.env.SHELL ? basename(process.env.SHELL) : undefined,
51
+ nodeVersion: process.version,
52
+ },
53
+ service: {
54
+ name: "computer-linker",
55
+ version: workspaceLinkerVersion(),
56
+ transports: ["stdio", "http"],
57
+ localUrl: capabilities.startup?.localMcpUrl ?? `http://${config.host ?? "127.0.0.1"}:${config.port ?? 3939}/mcp`,
58
+ publicUrl: capabilities.exposure?.publicMcpUrl ?? null,
59
+ },
60
+ scopes: registry.listDefinedWorkspaces().map((workspace) => ({
61
+ id: workspace.id,
62
+ name: workspace.name,
63
+ type: "folder",
64
+ roots: [workspace.path],
65
+ permissions: workspace.permissions,
66
+ policy: workspace.policy ?? {},
67
+ capabilityPolicy: workspaceCapabilityPolicy(workspace.permissions),
68
+ allowedOperations: allowedWorkspaceOperations(workspace.permissions),
69
+ })),
70
+ tools: {
71
+ ...(capabilities.toolReadiness && typeof capabilities.toolReadiness === "object" ? capabilities.toolReadiness : {}),
72
+ screenshot: screenshotCapability(),
73
+ },
74
+ operationContract: computerOperationContract,
75
+ operationRegistry: publicComputerOperationRegistry(),
76
+ compatibilityOperationRegistry: capabilities.operationRegistry,
77
+ mcpToolSurface: {
78
+ active: activeMcpToolSurface,
79
+ exposedTools: exposedMcpTools(activeMcpToolSurface),
80
+ compatibilityOptIn: "COMPUTER_LINKER_MCP_TOOL_SURFACE=compatibility",
81
+ },
82
+ compatibility: {
83
+ workspaceTools: [...compatibilityMcpTools],
84
+ genericTools: [...genericMcpTools],
85
+ },
86
+ status: {
87
+ ready: true,
88
+ blockingReasons: [],
89
+ warnings: [],
90
+ },
91
+ };
92
+ }
93
+ export function getMcpClientSetup(options = {}) {
94
+ const config = loadConfig();
95
+ const profile = connectionProfile(config, options.includeSecrets === true);
96
+ const tunnelSnapshots = options.tunnels ?? listTunnelProcesses();
97
+ const activeOpenAiTunnel = runningOpenAiTunnel(tunnelSnapshots);
98
+ const activeOpenAiTunnelId = activeOpenAiTunnel ? openAiTunnelIdFromSnapshot(activeOpenAiTunnel) : undefined;
99
+ const detectedPublicUrl = runningTunnelPublicUrl(tunnelSnapshots);
100
+ const publicBaseUrl = detectedPublicUrl ?? config.publicBaseUrl;
101
+ const publicMcpUrl = publicBaseUrl ? new URL("/mcp", publicBaseUrl).href : null;
102
+ const publicBaseUrlSource = detectedPublicUrl
103
+ ? "running-tunnel"
104
+ : config.publicBaseUrl ? "configured" : null;
105
+ const remoteReady = Boolean((activeOpenAiTunnel || publicMcpUrl?.startsWith("https://")) && config.ownerToken);
106
+ const blockingReasons = [];
107
+ const remoteBlockingReasons = [
108
+ ...(!config.ownerToken ? ["ownerToken is required before exposing HTTP MCP beyond loopback"] : []),
109
+ ...(!activeOpenAiTunnel && !publicMcpUrl ? ["No public MCP URL is configured or detected"] : []),
110
+ ...(!activeOpenAiTunnel && publicMcpUrl && !publicMcpUrl.startsWith("https://") ? ["public MCP URL must use https:// for remote clients"] : []),
111
+ ];
112
+ const warnings = [
113
+ ...(!activeOpenAiTunnel && !publicMcpUrl ? ["No public MCP URL is configured or detected; local stdio/loopback clients can still connect."] : []),
114
+ ...(detectedPublicUrl && detectedPublicUrl !== config.publicBaseUrl ? ["Detected tunnel URL is temporary until saved as publicBaseUrl."] : []),
115
+ ];
116
+ const localBearerHeader = config.ownerToken ? profile.http.auth.header ?? "Authorization: Bearer <ownerToken>" : null;
117
+ return {
118
+ kind: "computer-linker-mcp-client-setup",
119
+ schemaVersion: 1,
120
+ machineId: config.machineId,
121
+ machineName: config.machineName,
122
+ localReady: true,
123
+ ready: true,
124
+ remoteReady,
125
+ connection: {
126
+ stdio: profile.stdio,
127
+ localMcpUrl: profile.http.localMcpUrl,
128
+ publicMcpUrl,
129
+ publicBaseUrl: publicBaseUrl ?? null,
130
+ publicBaseUrlSource,
131
+ tunnel: activeOpenAiTunnel
132
+ ? {
133
+ provider: "openai",
134
+ mode: "secure-mcp-tunnel",
135
+ status: "running",
136
+ tunnelId: activeOpenAiTunnelId ?? activeOpenAiTunnel.id,
137
+ localMcpTarget: profile.http.localMcpUrl,
138
+ publicUrlRequired: false,
139
+ }
140
+ : null,
141
+ },
142
+ auth: {
143
+ mode: activeOpenAiTunnel ? "openai-secure-tunnel" : config.ownerToken ? "owner-token-or-oauth" : "loopback-only",
144
+ bearerHeader: activeOpenAiTunnel ? null : localBearerHeader,
145
+ alternateBearerHeader: config.ownerToken
146
+ ? activeOpenAiTunnel
147
+ ? null
148
+ : options.includeSecrets === true
149
+ ? `x-computer-linker-token: ${config.ownerToken}`
150
+ : "x-computer-linker-token: <ownerToken>"
151
+ : null,
152
+ localBearerHeader,
153
+ oauthDiscovery: publicMcpUrl && config.ownerToken && config.publicBaseUrl
154
+ ? {
155
+ authorizationServerMetadataUrl: new URL("/.well-known/oauth-authorization-server", config.publicBaseUrl).href,
156
+ protectedResourceMetadataUrl: new URL("/.well-known/oauth-protected-resource/mcp", config.publicBaseUrl).href,
157
+ scopes: ["computer-linker"],
158
+ }
159
+ : null,
160
+ notes: activeOpenAiTunnel
161
+ ? ["OpenAI tunnel-client forwards the owner token to the private local MCP server; do not paste a bearer token into ChatGPT Tunnel mode."]
162
+ : [],
163
+ },
164
+ tools: [...genericMcpTools],
165
+ operationShape: {
166
+ tool: "computer_operation",
167
+ contract: computerOperationContract,
168
+ registry: publicComputerOperationRegistry(),
169
+ envelope: {
170
+ scope: "app",
171
+ op: "file.read",
172
+ target: "README.md",
173
+ input: {},
174
+ options: { maxBytes: 65536 },
175
+ },
176
+ },
177
+ firstPrompt: "Call get_computer_info, choose an allowed scope, then use computer_operation with dotted ops from computerOperationRegistry and the stable scope/op/target/input/options envelope. Use get_operation_history to inspect what happened. Do not call compatibility workspace tools unless the server explicitly exposes them.",
178
+ agentInstructions: genericAgentInstructions,
179
+ blockingReasons,
180
+ remoteBlockingReasons,
181
+ warnings,
182
+ nextActions: mcpClientSetupNextActions(remoteReady, remoteBlockingReasons, warnings, activeOpenAiTunnelId),
183
+ };
184
+ }
185
+ export async function runComputerOperation(envelope) {
186
+ const operationId = `op_${randomUUID()}`;
187
+ const startedAt = new Date();
188
+ const started = performance.now();
189
+ const inputScope = optionalString(envelope.scope) ?? "";
190
+ const inputOp = optionalString(envelope.op) ?? "";
191
+ try {
192
+ const scope = stringValue(envelope.scope, "scope");
193
+ const op = stringValue(envelope.op, "op");
194
+ const workspaceOperation = computerOperationMap[op] ?? op;
195
+ const input = normalizeComputerOperationInput(envelope, workspaceOperation);
196
+ const registry = new WorkspaceRegistry(loadConfig());
197
+ const workspace = await registry.openWorkspace(scope);
198
+ return {
199
+ ok: true,
200
+ operationId,
201
+ scope,
202
+ op,
203
+ startedAt: startedAt.toISOString(),
204
+ durationMs: elapsedMs(started),
205
+ data: await runWorkspaceOperation(registry, workspace, input),
206
+ warnings: [],
207
+ };
208
+ }
209
+ catch (error) {
210
+ return {
211
+ ok: false,
212
+ operationId,
213
+ scope: inputScope,
214
+ op: inputOp,
215
+ startedAt: startedAt.toISOString(),
216
+ durationMs: elapsedMs(started),
217
+ error: computerOperationError(error),
218
+ warnings: [],
219
+ };
220
+ }
221
+ }
222
+ export function getOperationHistory(input) {
223
+ const view = optionalString(input.view) ?? "last";
224
+ const scope = optionalString(input.scope ?? input.workspace ?? input.workspaceId);
225
+ if (view === "raw") {
226
+ return {
227
+ events: readAuditEvents({
228
+ workspaceId: scope,
229
+ query: optionalString(input.q ?? input.query),
230
+ limit: optionalPositiveInteger(input.limit),
231
+ }),
232
+ };
233
+ }
234
+ return historyInsight({
235
+ view,
236
+ workspaceId: scope,
237
+ query: optionalString(input.q ?? input.query),
238
+ limit: optionalPositiveInteger(input.limit),
239
+ });
240
+ }
241
+ export function normalizeComputerOperationInput(envelope, workspaceOperation = stringValue(envelope.op, "op")) {
242
+ const sourceOp = stringValue(envelope.op, "op");
243
+ const mappedInput = {
244
+ ...(envelope.input ?? {}),
245
+ ...historyViewInput(sourceOp),
246
+ };
247
+ return normalizeWorkspaceOperationInput({
248
+ operation: workspaceOperation,
249
+ target: envelope.target,
250
+ input: mappedInput,
251
+ options: envelope.options ?? {},
252
+ });
253
+ }
254
+ export async function computerOperationAuditFields(envelope) {
255
+ const scope = optionalString(envelope.scope);
256
+ const op = optionalString(envelope.op);
257
+ let workspaceOperationInput;
258
+ try {
259
+ workspaceOperationInput = op ? normalizeComputerOperationInput(envelope, computerOperationMap[op] ?? op) : undefined;
260
+ }
261
+ catch {
262
+ workspaceOperationInput = undefined;
263
+ }
264
+ const fields = {
265
+ workspaceRef: scope,
266
+ operation: op,
267
+ target: optionalString(envelope.target),
268
+ ...(workspaceOperationInput ? workspaceOperationAuditFields(workspaceOperationInput) : {}),
269
+ };
270
+ fields.operation = op ?? fields.operation;
271
+ fields.target = optionalString(envelope.target) ?? fields.target;
272
+ if (!scope)
273
+ return fields;
274
+ try {
275
+ const registry = new WorkspaceRegistry(loadConfig());
276
+ const workspace = await registry.openWorkspace(scope);
277
+ return {
278
+ ...fields,
279
+ workspaceId: workspace.exposedPath.id,
280
+ workspaceRoot: workspace.root,
281
+ workspaceRef: scope,
282
+ };
283
+ }
284
+ catch {
285
+ return fields;
286
+ }
287
+ }
288
+ export function computerOperationName(op) {
289
+ return computerOperationMap[op] ?? op;
290
+ }
291
+ function historyViewInput(op) {
292
+ return historyViewMap[op] ? { view: historyViewMap[op] } : {};
293
+ }
294
+ function stringValue(value, name) {
295
+ const text = optionalString(value);
296
+ if (!text)
297
+ throw new Error(`${name} is required`);
298
+ return text;
299
+ }
300
+ function optionalString(value) {
301
+ const text = String(value ?? "").trim();
302
+ return text || undefined;
303
+ }
304
+ function optionalPositiveInteger(value) {
305
+ const text = optionalString(value);
306
+ if (!text)
307
+ return undefined;
308
+ const parsed = Number.parseInt(text, 10);
309
+ return Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, 1000) : undefined;
310
+ }
311
+ function elapsedMs(started) {
312
+ return Math.max(0, Math.round(performance.now() - started));
313
+ }
314
+ function computerOperationError(error) {
315
+ const message = error instanceof Error ? error.message : String(error);
316
+ return {
317
+ code: computerOperationErrorCode(error, message),
318
+ message,
319
+ retryable: isRetryableComputerOperationError(message),
320
+ details: error instanceof Error ? { name: error.name } : {},
321
+ };
322
+ }
323
+ function computerOperationErrorCode(error, message) {
324
+ if (/(^|\s)(scope|op|path|query|command|prompt|processId|operationName|workspace) is required\b/.test(message)) {
325
+ return "invalid_request";
326
+ }
327
+ if (/Unknown configured workspace|Unknown workspaceId/.test(message))
328
+ return "unknown_scope";
329
+ if (/outside workspace|outside exposed path|outside workspace root|resolves outside workspace|outside exposed paths/i.test(message)) {
330
+ return "path_out_of_scope";
331
+ }
332
+ if (/operation must be one of|Unknown operation|cannot execute operation|Unsupported Codex workflow/i.test(message)) {
333
+ return "unknown_operation";
334
+ }
335
+ if (error instanceof PermissionDeniedError || /permission is disabled|permission denied/i.test(message)) {
336
+ return "permission_denied";
337
+ }
338
+ if (/os permission|required.*permission|Screen Recording permission/i.test(message))
339
+ return "os_permission_required";
340
+ if (/provider.*unavailable|unavailable.*provider/i.test(message))
341
+ return "provider_unavailable";
342
+ if (/not implemented|unsupported platform|unsupported|not supported/i.test(message))
343
+ return "unsupported_platform";
344
+ if (/timed out|timeout/i.test(message))
345
+ return "timeout";
346
+ if (/Unknown process|process not found/i.test(message))
347
+ return "process_not_found";
348
+ return "execution_failed";
349
+ }
350
+ function isRetryableComputerOperationError(message) {
351
+ return /timed out|timeout|os permission|required.*permission|provider.*unavailable/i.test(message);
352
+ }
353
+ function runningTunnelPublicUrl(tunnels) {
354
+ return tunnels
355
+ .filter((tunnel) => tunnel.status === "running")
356
+ .map((tunnel) => tunnel.publicUrl)
357
+ .find((url) => Boolean(url));
358
+ }
359
+ function runningOpenAiTunnel(tunnels) {
360
+ return tunnels.find((tunnel) => tunnel.status === "running" && tunnel.provider === "openai");
361
+ }
362
+ function openAiTunnelIdFromSnapshot(tunnel) {
363
+ const args = Array.isArray(tunnel.args) ? tunnel.args : [];
364
+ for (let index = 0; index < args.length; index += 1) {
365
+ if (args[index] === "--control-plane.tunnel-id" && args[index + 1])
366
+ return args[index + 1];
367
+ }
368
+ return undefined;
369
+ }
370
+ function mcpClientSetupNextActions(remoteReady, remoteBlockingReasons, warnings, openAiTunnelId) {
371
+ if (remoteReady) {
372
+ if (openAiTunnelId) {
373
+ return [`Use OpenAI/ChatGPT Tunnel mode and select or paste ${openAiTunnelId}; the target MCP path remains /mcp.`, "Then call get_computer_info."];
374
+ }
375
+ return ["Use the public MCP URL with bearer auth or OAuth, then call get_computer_info."];
376
+ }
377
+ const actions = new Set();
378
+ actions.add("For local clients, use stdio or the loopback MCP URL.");
379
+ for (const reason of remoteBlockingReasons)
380
+ actions.add(`For remote clients, resolve: ${reason}`);
381
+ for (const warning of warnings)
382
+ actions.add(`Review: ${warning}`);
383
+ return [...actions];
384
+ }
@@ -0,0 +1,45 @@
1
+ import { type PublicWorkspaceOperationRegistryEntry, type WorkspaceOperationName } from "./workspace-operations.js";
2
+ export interface ComputerOperationEnvelope {
3
+ scope?: string;
4
+ op?: string;
5
+ target?: string;
6
+ input?: Record<string, unknown>;
7
+ options?: Record<string, unknown>;
8
+ }
9
+ export interface ComputerOperationRegistryEntry {
10
+ op: string;
11
+ category: "file" | "code" | "git" | "package" | "command" | "process" | "codex" | "history" | "screen";
12
+ permission: PublicWorkspaceOperationRegistryEntry["permission"];
13
+ capabilities: PublicWorkspaceOperationRegistryEntry["capabilities"];
14
+ boundary: PublicWorkspaceOperationRegistryEntry["boundary"];
15
+ description: string;
16
+ target?: string;
17
+ requiredInput: string[];
18
+ optionalInput: string[];
19
+ options: string[];
20
+ backendOperation: WorkspaceOperationName;
21
+ legacyWorkspaceOperation: WorkspaceOperationName;
22
+ example: ComputerOperationEnvelope;
23
+ }
24
+ export interface ComputerOperationContract {
25
+ version: 1;
26
+ mcp: {
27
+ tool: "computer_operation";
28
+ requiredFields: ["scope", "op"];
29
+ };
30
+ jsonApi: {
31
+ endpoint: "POST /api/v1/control";
32
+ action: "computer_operation";
33
+ requiredFields: ["action", "scope", "op"];
34
+ };
35
+ envelope: ComputerOperationEnvelope;
36
+ guidance: string[];
37
+ compatibility: {
38
+ acceptsLegacyWorkspaceOps: true;
39
+ legacyRegistry: "operationRegistry";
40
+ };
41
+ }
42
+ export declare const computerOperationMap: Record<string, WorkspaceOperationName>;
43
+ export declare const computerOperationContract: ComputerOperationContract;
44
+ export declare const computerOperationRegistry: ComputerOperationRegistryEntry[];
45
+ export declare function publicComputerOperationRegistry(): ComputerOperationRegistryEntry[];
@@ -0,0 +1,179 @@
1
+ import { workspaceOperationEntry, } from "./workspace-operations.js";
2
+ import { screenshotCapability } from "./screenshot.js";
3
+ const computerOperationDefinitions = [
4
+ { op: "file.stat", category: "file", backendOperation: "stat", target: "path", description: "Return metadata for one scoped file or directory." },
5
+ { op: "file.list", category: "file", backendOperation: "list_details", target: "path", description: "List directory entries with type, size, and modified time." },
6
+ { op: "file.tree", category: "file", backendOperation: "tree", target: "path", description: "List a bounded recursive tree for codebase orientation." },
7
+ { op: "file.read", category: "file", backendOperation: "read", target: "path", description: "Read one UTF-8 file with optional byte or line bounds." },
8
+ { op: "file.read_many", category: "file", backendOperation: "read_many", description: "Read several UTF-8 files in one bounded scoped call." },
9
+ { op: "file.write", category: "file", backendOperation: "write", target: "path", description: "Create or overwrite one UTF-8 file inside the scope." },
10
+ { op: "file.create", category: "file", backendOperation: "create_file", target: "path", description: "Create one new UTF-8 file inside the scope and fail if it already exists." },
11
+ { op: "file.patch", category: "file", backendOperation: "patch", target: "path", description: "Apply a validated unified diff inside the scope." },
12
+ { op: "file.move", category: "file", backendOperation: "move", target: "fromPath", description: "Move or rename a scoped file or directory." },
13
+ { op: "file.delete", category: "file", backendOperation: "delete", target: "path", description: "Delete a scoped file or directory." },
14
+ { op: "file.find", category: "file", backendOperation: "find_files", target: "path", description: "Find scoped file paths by pattern." },
15
+ { op: "file.search", category: "file", backendOperation: "search_text", target: "path", description: "Search text quickly, preferring ripgrep when available." },
16
+ { op: "code.context", category: "code", backendOperation: "coding_context", target: "path", description: "Return a bounded code-oriented workspace context." },
17
+ { op: "code.search_symbols", category: "code", backendOperation: "search_symbols", target: "path", description: "Find common code symbols such as functions, classes, interfaces, and exports." },
18
+ { op: "git.status", category: "git", backendOperation: "repo_status", target: "path", description: "Inspect repository status and optional bounded diff." },
19
+ { op: "git.changes", category: "git", backendOperation: "git_changes", target: "path", description: "Return structured changed-file entries and counts." },
20
+ { op: "git.diff", category: "git", backendOperation: "git_diff", target: "path", description: "Return a bounded staged or unstaged diff." },
21
+ { op: "git.log", category: "git", backendOperation: "git_log", target: "path", description: "Return recent commits for a repository or pathspec." },
22
+ { op: "git.show", category: "git", backendOperation: "git_show", target: "path", description: "Return a bounded commit or object view." },
23
+ { op: "git.stage", category: "git", backendOperation: "git_stage", target: "path", description: "Stage scoped paths in the Git index." },
24
+ { op: "git.unstage", category: "git", backendOperation: "git_unstage", target: "path", description: "Unstage scoped paths from the Git index." },
25
+ { op: "git.commit", category: "git", backendOperation: "git_commit", target: "path", description: "Create a Git commit from staged scoped files." },
26
+ { op: "package.run", category: "package", backendOperation: "package_run", target: "path", description: "Run an existing package.json script in the scope." },
27
+ { op: "package.start", category: "package", backendOperation: "package_start", target: "path", description: "Start an existing package.json script as a managed process." },
28
+ { op: "command.run", category: "command", backendOperation: "command", target: "workingDirectory", description: "Run one bounded shell command in the scope." },
29
+ { op: "command.start", category: "command", backendOperation: "process_start", target: "workingDirectory", description: "Start a managed long-running shell process." },
30
+ { op: "command.read", category: "command", backendOperation: "process_read", target: "processId", description: "Read status and buffered output for a managed process." },
31
+ { op: "command.stop", category: "command", backendOperation: "process_stop", target: "processId", description: "Stop a managed process." },
32
+ { op: "command.list", category: "command", backendOperation: "process_list", description: "List managed shell and Codex processes for the scope." },
33
+ { op: "process.start", category: "process", backendOperation: "process_start", target: "workingDirectory", description: "Start a managed long-running shell process." },
34
+ { op: "process.read", category: "process", backendOperation: "process_read", target: "processId", description: "Read status and buffered output for a managed process." },
35
+ { op: "process.stop", category: "process", backendOperation: "process_stop", target: "processId", description: "Stop a managed process." },
36
+ { op: "process.list", category: "process", backendOperation: "process_list", description: "List managed processes for the scope." },
37
+ { op: "codex.run", category: "codex", backendOperation: "codex", target: "workingDirectory", description: "Run the local Codex CLI once in the scope." },
38
+ { op: "codex.start", category: "codex", backendOperation: "codex_start", target: "workingDirectory", description: "Start a managed Codex CLI task." },
39
+ { op: "codex.read", category: "codex", backendOperation: "codex_runs", target: "workflowId", description: "Read persisted Codex run records." },
40
+ { op: "codex.stop", category: "codex", backendOperation: "process_stop", target: "processId", description: "Stop a managed Codex process." },
41
+ { op: "codex.list", category: "codex", backendOperation: "codex_runs", description: "List recent persisted Codex run records." },
42
+ { op: "history.last", category: "history", backendOperation: "history_insight", description: "Return the latest redacted operation summary." },
43
+ { op: "history.timeline", category: "history", backendOperation: "history_insight", description: "Return a redacted operation timeline." },
44
+ { op: "history.sessions", category: "history", backendOperation: "history_insight", description: "Return recent session summaries." },
45
+ { op: "history.connections", category: "history", backendOperation: "history_insight", description: "Return tunnel and MCP connection summaries." },
46
+ { op: "history.failed_replay", category: "history", backendOperation: "history_insight", description: "Return failed-operation replay templates." },
47
+ { op: "history.debug_bundle", category: "history", backendOperation: "history_insight", description: "Export a redacted debug bundle." },
48
+ { op: "screen.list", category: "screen", backendOperation: "screen_list", description: "List screenshot provider readiness, displays, and capturable windows/processes when available." },
49
+ { op: "screen.capture", category: "screen", backendOperation: "screen_capture", target: "displayId", backendTarget: "path", description: "Capture the primary display or selected display when supported." },
50
+ { op: "screen.capture_window", category: "screen", backendOperation: "screen_capture_window", target: "windowId", backendTarget: "path", description: "Capture a visible window by id when supported." },
51
+ { op: "screen.capture_process", category: "screen", backendOperation: "screen_capture_process", target: "processIdOrName", backendTarget: "path", description: "Capture a visible window for a process id or process name when supported." },
52
+ ];
53
+ export const computerOperationMap = Object.fromEntries(computerOperationDefinitions.map((entry) => [entry.op, entry.backendOperation]));
54
+ const computerOperationExamples = {
55
+ "file.stat": { scope: "app", op: "file.stat", target: "README.md" },
56
+ "file.list": { scope: "app", op: "file.list", target: "src" },
57
+ "file.tree": { scope: "app", op: "file.tree", target: ".", options: { maxDepth: 2, maxEntries: 100 } },
58
+ "file.read": { scope: "app", op: "file.read", target: "README.md", options: { maxBytes: 65536 } },
59
+ "file.read_many": { scope: "app", op: "file.read_many", input: { paths: ["README.md", "package.json"] }, options: { maxBytes: 65536 } },
60
+ "file.write": { scope: "app", op: "file.write", target: "notes/todo.md", input: { content: "- item\n" } },
61
+ "file.create": { scope: "app", op: "file.create", target: "notes/todo.md", input: { content: "- item\n" } },
62
+ "file.patch": { scope: "app", op: "file.patch", input: { patch: "diff --git a/README.md b/README.md\n--- a/README.md\n+++ b/README.md\n@@ -1 +1 @@\n-old\n+new\n" } },
63
+ "file.move": { scope: "app", op: "file.move", input: { fromPath: "old.txt", toPath: "archive/old.txt" } },
64
+ "file.delete": { scope: "app", op: "file.delete", target: "tmp/output", input: { recursive: true } },
65
+ "file.find": { scope: "app", op: "file.find", target: ".", input: { pattern: "*.ts" }, options: { maxResults: 50 } },
66
+ "file.search": { scope: "app", op: "file.search", target: ".", input: { query: "TODO", glob: "*.ts" }, options: { maxResults: 20 } },
67
+ "code.context": { scope: "app", op: "code.context", target: ".", options: { maxDepth: 2, maxEntries: 100, maxBytes: 32768 } },
68
+ "code.search_symbols": { scope: "app", op: "code.search_symbols", target: ".", input: { query: "Workspace", glob: "*.ts" }, options: { maxResults: 50 } },
69
+ "git.status": { scope: "app", op: "git.status", target: ".", input: { includeDiff: true }, options: { maxBytes: 65536 } },
70
+ "git.changes": { scope: "app", op: "git.changes", target: "." },
71
+ "git.diff": { scope: "app", op: "git.diff", target: ".", input: { paths: ["src/index.ts"], staged: false }, options: { maxBytes: 65536 } },
72
+ "git.log": { scope: "app", op: "git.log", target: ".", input: { paths: ["src/index.ts"] }, options: { maxResults: 20 } },
73
+ "git.show": { scope: "app", op: "git.show", target: ".", input: { ref: "HEAD", paths: ["src/index.ts"] }, options: { maxBytes: 65536 } },
74
+ "git.stage": { scope: "app", op: "git.stage", target: ".", input: { paths: ["src/index.ts", "README.md"] } },
75
+ "git.unstage": { scope: "app", op: "git.unstage", target: ".", input: { paths: ["src/index.ts"] } },
76
+ "git.commit": { scope: "app", op: "git.commit", target: ".", input: { message: "Implement workspace search" } },
77
+ "package.run": { scope: "app", op: "package.run", target: ".", input: { script: "test", scriptArgs: ["--watch=false"] }, options: { timeoutSeconds: 120, maxOutputBytes: 200000 } },
78
+ "package.start": { scope: "app", op: "package.start", target: ".", input: { script: "dev", scriptArgs: ["--host", "127.0.0.1"] }, options: { timeoutSeconds: 3600, maxOutputBytes: 200000 } },
79
+ "command.run": { scope: "app", op: "command.run", target: ".", input: { command: "npm test" }, options: { timeoutSeconds: 600 } },
80
+ "command.start": { scope: "app", op: "command.start", target: ".", input: { command: "npm run dev" }, options: { timeoutSeconds: 3600 } },
81
+ "command.read": { scope: "app", op: "command.read", input: { processId: "proc_..." } },
82
+ "command.stop": { scope: "app", op: "command.stop", input: { processId: "proc_..." } },
83
+ "command.list": { scope: "app", op: "command.list" },
84
+ "process.start": { scope: "app", op: "process.start", target: ".", input: { command: "npm run dev" }, options: { timeoutSeconds: 3600 } },
85
+ "process.read": { scope: "app", op: "process.read", target: "proc_..." },
86
+ "process.stop": { scope: "app", op: "process.stop", target: "proc_..." },
87
+ "process.list": { scope: "app", op: "process.list" },
88
+ "codex.run": { scope: "app", op: "codex.run", target: ".", input: { prompt: "Inspect this repo and summarize failing tests." }, options: { timeoutSeconds: 1800 } },
89
+ "codex.start": { scope: "app", op: "codex.start", target: ".", input: { prompt: "Run tests and summarize failures." }, options: { timeoutSeconds: 1800 } },
90
+ "codex.read": { scope: "app", op: "codex.read", input: { workflowId: "codex_fix_..." } },
91
+ "codex.stop": { scope: "app", op: "codex.stop", target: "proc_..." },
92
+ "codex.list": { scope: "app", op: "codex.list", options: { maxResults: 10 } },
93
+ "history.last": { scope: "app", op: "history.last" },
94
+ "history.timeline": { scope: "app", op: "history.timeline", options: { maxResults: 50 } },
95
+ "history.sessions": { scope: "app", op: "history.sessions", options: { maxResults: 20 } },
96
+ "history.connections": { scope: "app", op: "history.connections", options: { maxResults: 20 } },
97
+ "history.failed_replay": { scope: "app", op: "history.failed_replay", options: { maxResults: 20 } },
98
+ "history.debug_bundle": { scope: "app", op: "history.debug_bundle", options: { maxResults: 100 } },
99
+ "screen.list": { scope: "app", op: "screen.list" },
100
+ "screen.capture": { scope: "app", op: "screen.capture", target: "primary", options: { returnMode: "fileRef", format: "png" } },
101
+ "screen.capture_window": { scope: "app", op: "screen.capture_window", target: "window-1", options: { returnMode: "fileRef", format: "png" } },
102
+ "screen.capture_process": { scope: "app", op: "screen.capture_process", target: "Terminal", options: { returnMode: "fileRef", format: "png" } },
103
+ };
104
+ export const computerOperationContract = {
105
+ version: 1,
106
+ mcp: {
107
+ tool: "computer_operation",
108
+ requiredFields: ["scope", "op"],
109
+ },
110
+ jsonApi: {
111
+ endpoint: "POST /api/v1/control",
112
+ action: "computer_operation",
113
+ requiredFields: ["action", "scope", "op"],
114
+ },
115
+ envelope: {
116
+ scope: "app",
117
+ op: "file.read",
118
+ target: "README.md",
119
+ input: {},
120
+ options: { maxBytes: 65536 },
121
+ },
122
+ guidance: [
123
+ "Keep the outer envelope stable: scope, op, target, input, options.",
124
+ "Use the generic dotted op names for new clients, such as file.read, file.search, code.context, git.diff, package.run, command.run, process.start, codex.run, screen.capture, and history.last.",
125
+ "Put operation-specific payload in input and bounds such as maxBytes, maxResults, and timeoutSeconds in options.",
126
+ "Choose scope from get_computer_info.scopes and check the returned operation registry before write, command, or Codex operations.",
127
+ ],
128
+ compatibility: {
129
+ acceptsLegacyWorkspaceOps: true,
130
+ legacyRegistry: "operationRegistry",
131
+ },
132
+ };
133
+ export const computerOperationRegistry = computerOperationDefinitions.map((definition) => {
134
+ const backend = workspaceOperationEntry(definition.backendOperation);
135
+ const optionFields = new Set(["maxBytes", "maxOutputBytes", "maxResults", "timeoutSeconds", "maxDepth", "maxEntries", "startLine", "lineCount", "beforeContext", "afterContext", "format", "returnMode", "maxWidth", "maxHeight"]);
136
+ const backendTarget = definition.backendTarget ?? definition.target;
137
+ const requiredInput = backend.requiredFields.filter((field) => field !== backendTarget);
138
+ const optionalInput = backend.optionalFields.filter((field) => field !== backendTarget && !optionFields.has(field));
139
+ const options = backend.optionalFields.filter((field) => optionFields.has(field));
140
+ return {
141
+ op: definition.op,
142
+ category: definition.category,
143
+ permission: backend.permission,
144
+ capabilities: backend.capabilities,
145
+ boundary: backend.boundary,
146
+ description: definition.description,
147
+ target: definition.target,
148
+ requiredInput,
149
+ optionalInput,
150
+ options,
151
+ backendOperation: definition.backendOperation,
152
+ legacyWorkspaceOperation: definition.backendOperation,
153
+ example: computerOperationExamples[definition.op] ?? {
154
+ ...computerOperationContract.envelope,
155
+ op: definition.op,
156
+ },
157
+ };
158
+ });
159
+ export function publicComputerOperationRegistry() {
160
+ return computerOperationRegistry.filter((operation) => operationSupportedByCurrentRuntime(operation));
161
+ }
162
+ function operationSupportedByCurrentRuntime(operation) {
163
+ if (operation.op === "screen.list")
164
+ return true;
165
+ const screenMode = screenCaptureModeForOperation(operation.backendOperation);
166
+ if (!screenMode)
167
+ return true;
168
+ const capability = screenshotCapability();
169
+ return capability.supported && capability.modes.includes(screenMode);
170
+ }
171
+ function screenCaptureModeForOperation(operation) {
172
+ if (operation === "screen_capture")
173
+ return "display";
174
+ if (operation === "screen_capture_window")
175
+ return "window";
176
+ if (operation === "screen_capture_process")
177
+ return "process";
178
+ return undefined;
179
+ }