@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,61 @@
1
+ import { type LocalPortConfig, type ResolvedExposedPath } from "./permissions.js";
2
+ export interface Workspace {
3
+ id: string;
4
+ root: string;
5
+ exposedPath: ResolvedExposedPath;
6
+ }
7
+ export interface WorkspaceCandidate {
8
+ path: string;
9
+ id: string;
10
+ name: string;
11
+ permissions: ResolvedExposedPath["permissions"];
12
+ }
13
+ export interface WorkspacePathInfo {
14
+ path: string;
15
+ name: string;
16
+ type: "file" | "directory" | "symlink" | "other";
17
+ size: number;
18
+ modifiedAt: string;
19
+ }
20
+ export interface WorkspaceTreeOptions {
21
+ maxDepth?: number;
22
+ maxEntries?: number;
23
+ includeFiles?: boolean;
24
+ }
25
+ export interface WorkspaceInstructionFile {
26
+ path: string;
27
+ name: string;
28
+ content: string;
29
+ size: number;
30
+ truncated: boolean;
31
+ }
32
+ export interface WorkspaceInstructionsOptions {
33
+ maxBytes?: number;
34
+ }
35
+ export declare class WorkspaceRegistry {
36
+ private readonly config;
37
+ private readonly workspaces;
38
+ constructor(config: LocalPortConfig);
39
+ listDefinedWorkspaces(): ResolvedExposedPath[];
40
+ listWorkspaceCandidates(): Promise<WorkspaceCandidate[]>;
41
+ openWorkspace(workspaceRef: string): Promise<Workspace>;
42
+ private findWorkspaceByRef;
43
+ getWorkspace(workspaceId: string): Workspace;
44
+ resolvePath(workspace: Workspace, inputPath: string): string;
45
+ resolveExistingPath(workspace: Workspace, inputPath: string): Promise<string>;
46
+ resolveWritablePath(workspace: Workspace, inputPath: string): Promise<string>;
47
+ readFile(workspaceId: string, path: string): Promise<string>;
48
+ writeFile(workspaceId: string, path: string, content: string): Promise<void>;
49
+ createFile(workspaceId: string, path: string, content: string): Promise<void>;
50
+ editFile(workspaceId: string, path: string, oldText: string, newText: string): Promise<number>;
51
+ listDirectory(workspaceId: string, path: string): Promise<string[]>;
52
+ listDirectoryEntries(workspaceId: string, path: string): Promise<WorkspacePathInfo[]>;
53
+ tree(workspaceId: string, path: string, options?: WorkspaceTreeOptions): Promise<WorkspacePathInfo[]>;
54
+ instructions(workspaceId: string, path: string, options?: WorkspaceInstructionsOptions): Promise<WorkspaceInstructionFile[]>;
55
+ statPath(workspaceId: string, path: string): Promise<WorkspacePathInfo>;
56
+ private resolveInstructionTarget;
57
+ createDirectory(workspaceId: string, path: string): Promise<void>;
58
+ deletePath(workspaceId: string, path: string, recursive?: boolean): Promise<void>;
59
+ movePath(workspaceId: string, fromPath: string, toPath: string): Promise<void>;
60
+ }
61
+ export declare function formatWorkspacePath(path: string, workspace: Workspace): string;
@@ -0,0 +1,353 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { lstat, mkdir, opendir, readFile, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, join, relative, resolve, sep } from "node:path";
4
+ import { assertPermission, isPathInsideRoot, } from "./permissions.js";
5
+ import { assertNonSensitiveWorkspacePath } from "./sensitive-files.js";
6
+ export class WorkspaceRegistry {
7
+ config;
8
+ workspaces = new Map();
9
+ constructor(config) {
10
+ this.config = config;
11
+ }
12
+ listDefinedWorkspaces() {
13
+ return this.config.workspaces.map((entry) => ({
14
+ ...entry,
15
+ path: resolve(entry.path),
16
+ }));
17
+ }
18
+ async listWorkspaceCandidates() {
19
+ return this.listDefinedWorkspaces()
20
+ .filter((workspace) => workspace.permissions.read)
21
+ .map((workspace) => ({
22
+ id: workspace.id,
23
+ name: workspace.name,
24
+ path: workspace.path,
25
+ permissions: workspace.permissions,
26
+ }))
27
+ .sort((a, b) => a.id.localeCompare(b.id));
28
+ }
29
+ async openWorkspace(workspaceRef) {
30
+ const exposedPath = this.findWorkspaceByRef(workspaceRef);
31
+ assertPermission(exposedPath, "read");
32
+ await mkdir(exposedPath.path, { recursive: true });
33
+ if (!(await isDirectory(exposedPath.path))) {
34
+ throw new Error(`Workspace root must be a directory: ${exposedPath.path}`);
35
+ }
36
+ const realRoot = await realpath(exposedPath.path);
37
+ const workspace = {
38
+ id: `ws_${randomUUID()}`,
39
+ root: realRoot,
40
+ exposedPath: {
41
+ ...exposedPath,
42
+ path: realRoot,
43
+ },
44
+ };
45
+ this.workspaces.set(workspace.id, workspace);
46
+ return workspace;
47
+ }
48
+ findWorkspaceByRef(workspaceRef) {
49
+ const resolvedRef = resolve(workspaceRef);
50
+ const workspace = this.listDefinedWorkspaces().find((entry) => entry.id === workspaceRef ||
51
+ entry.name === workspaceRef ||
52
+ entry.path === resolvedRef);
53
+ if (!workspace) {
54
+ throw new Error(`Unknown configured workspace: ${workspaceRef}`);
55
+ }
56
+ return workspace;
57
+ }
58
+ getWorkspace(workspaceId) {
59
+ const workspace = this.workspaces.get(workspaceId);
60
+ if (!workspace)
61
+ throw new Error(`Unknown workspaceId: ${workspaceId}`);
62
+ return workspace;
63
+ }
64
+ resolvePath(workspace, inputPath) {
65
+ const absolutePath = resolve(workspace.root, inputPath);
66
+ if (!isPathInsideRoot(absolutePath, workspace.root)) {
67
+ throw new Error(`Path is outside workspace root: ${inputPath}`);
68
+ }
69
+ if (!isPathInsideRoot(absolutePath, workspace.exposedPath.path)) {
70
+ throw new Error(`Path is outside exposed path: ${inputPath}`);
71
+ }
72
+ return absolutePath;
73
+ }
74
+ async resolveExistingPath(workspace, inputPath) {
75
+ const absolutePath = this.resolvePath(workspace, inputPath);
76
+ await assertRealPathInside(workspace, absolutePath, inputPath);
77
+ return absolutePath;
78
+ }
79
+ async resolveWritablePath(workspace, inputPath) {
80
+ const absolutePath = this.resolvePath(workspace, inputPath);
81
+ try {
82
+ await assertRealPathInside(workspace, absolutePath, inputPath);
83
+ }
84
+ catch (error) {
85
+ if (!isNotFoundError(error))
86
+ throw error;
87
+ if (await pathExists(absolutePath))
88
+ throw error;
89
+ await assertRealPathInside(workspace, dirname(absolutePath), dirname(inputPath));
90
+ }
91
+ return absolutePath;
92
+ }
93
+ async readFile(workspaceId, path) {
94
+ const workspace = this.getWorkspace(workspaceId);
95
+ assertPermission(workspace.exposedPath, "read");
96
+ const absolutePath = await this.resolveExistingPath(workspace, path);
97
+ assertNonSensitiveWorkspacePath(formatWorkspacePath(absolutePath, workspace), "read");
98
+ return readFile(absolutePath, "utf8");
99
+ }
100
+ async writeFile(workspaceId, path, content) {
101
+ const workspace = this.getWorkspace(workspaceId);
102
+ assertPermission(workspace.exposedPath, "write");
103
+ const absolutePath = await this.resolveWritablePath(workspace, path);
104
+ await mkdir(dirname(absolutePath), { recursive: true });
105
+ await writeFile(absolutePath, content, "utf8");
106
+ }
107
+ async createFile(workspaceId, path, content) {
108
+ const workspace = this.getWorkspace(workspaceId);
109
+ assertPermission(workspace.exposedPath, "write");
110
+ const absolutePath = await this.resolveWritablePath(workspace, path);
111
+ await mkdir(dirname(absolutePath), { recursive: true });
112
+ try {
113
+ await writeFile(absolutePath, content, { encoding: "utf8", flag: "wx" });
114
+ }
115
+ catch (error) {
116
+ if (error instanceof Error && "code" in error && error.code === "EEXIST") {
117
+ throw new Error(`File already exists: ${path}`);
118
+ }
119
+ throw error;
120
+ }
121
+ }
122
+ async editFile(workspaceId, path, oldText, newText) {
123
+ const current = await this.readFile(workspaceId, path);
124
+ const workspace = this.getWorkspace(workspaceId);
125
+ assertPermission(workspace.exposedPath, "write");
126
+ const matches = current.split(oldText).length - 1;
127
+ if (matches !== 1) {
128
+ throw new Error(`edit_file expected exactly one match, found ${matches}`);
129
+ }
130
+ await this.writeFile(workspaceId, path, current.replace(oldText, newText));
131
+ return matches;
132
+ }
133
+ async listDirectory(workspaceId, path) {
134
+ return (await this.listDirectoryEntries(workspaceId, path))
135
+ .map((entry) => `${entry.name}${entry.type === "directory" ? "/" : ""}`);
136
+ }
137
+ async listDirectoryEntries(workspaceId, path) {
138
+ const workspace = this.getWorkspace(workspaceId);
139
+ assertPermission(workspace.exposedPath, "read");
140
+ const directory = await this.resolveExistingPath(workspace, path);
141
+ const entries = await opendir(directory);
142
+ const results = [];
143
+ for await (const entry of entries) {
144
+ results.push(await pathInfo(join(directory, entry.name), workspace));
145
+ }
146
+ return results.sort((a, b) => a.name.localeCompare(b.name));
147
+ }
148
+ async tree(workspaceId, path, options = {}) {
149
+ const workspace = this.getWorkspace(workspaceId);
150
+ assertPermission(workspace.exposedPath, "read");
151
+ const root = await this.resolveExistingPath(workspace, path);
152
+ const maxDepth = normalizePositiveInteger(options.maxDepth, 2, 1000);
153
+ const maxEntries = normalizePositiveInteger(options.maxEntries, 200, 1000);
154
+ const includeFiles = options.includeFiles ?? true;
155
+ const results = [];
156
+ const walk = async (directory, depth) => {
157
+ if (results.length >= maxEntries)
158
+ return;
159
+ let entries;
160
+ try {
161
+ entries = await opendir(directory);
162
+ }
163
+ catch {
164
+ return;
165
+ }
166
+ const paths = [];
167
+ for await (const entry of entries) {
168
+ if (entry.isDirectory() && SKIPPED_TREE_DIRECTORIES.has(entry.name))
169
+ continue;
170
+ paths.push(join(directory, entry.name));
171
+ }
172
+ paths.sort((a, b) => basename(a).localeCompare(basename(b)));
173
+ for (const entryPath of paths) {
174
+ if (results.length >= maxEntries)
175
+ return;
176
+ const info = await pathInfo(entryPath, workspace);
177
+ if (includeFiles || info.type === "directory")
178
+ results.push(info);
179
+ if (info.type === "directory" && depth < maxDepth) {
180
+ await walk(entryPath, depth + 1);
181
+ }
182
+ }
183
+ };
184
+ await walk(root, 1);
185
+ return results;
186
+ }
187
+ async instructions(workspaceId, path, options = {}) {
188
+ const workspace = this.getWorkspace(workspaceId);
189
+ assertPermission(workspace.exposedPath, "read");
190
+ const target = await this.resolveInstructionTarget(workspace, path);
191
+ const directory = await instructionSearchDirectory(target);
192
+ const maxBytes = normalizePositiveInteger(options.maxBytes, 64 * 1024, 256 * 1024);
193
+ const files = [];
194
+ for (const directoryPath of ancestorDirectories(workspace.root, directory)) {
195
+ for (const name of INSTRUCTION_FILE_NAMES) {
196
+ const instructionPath = join(directoryPath, name);
197
+ try {
198
+ const info = await lstat(instructionPath);
199
+ if (!info.isFile())
200
+ continue;
201
+ const content = await readFile(instructionPath, "utf8");
202
+ files.push({
203
+ path: formatWorkspacePath(instructionPath, workspace),
204
+ name,
205
+ content: content.slice(0, maxBytes),
206
+ size: info.size,
207
+ truncated: info.size > maxBytes,
208
+ });
209
+ }
210
+ catch {
211
+ // Missing instruction files are expected in most directories.
212
+ }
213
+ }
214
+ }
215
+ return files;
216
+ }
217
+ async statPath(workspaceId, path) {
218
+ const workspace = this.getWorkspace(workspaceId);
219
+ assertPermission(workspace.exposedPath, "read");
220
+ const absolutePath = this.resolvePath(workspace, path);
221
+ await assertRealPathInside(workspace, absolutePath, path);
222
+ return pathInfo(absolutePath, workspace);
223
+ }
224
+ async resolveInstructionTarget(workspace, inputPath) {
225
+ const absolutePath = this.resolvePath(workspace, inputPath);
226
+ try {
227
+ await assertRealPathInside(workspace, absolutePath, inputPath);
228
+ }
229
+ catch (error) {
230
+ if (!isNotFoundError(error))
231
+ throw error;
232
+ if (await pathExists(absolutePath))
233
+ throw error;
234
+ await assertRealPathInside(workspace, await nearestExistingParent(absolutePath), inputPath);
235
+ }
236
+ return absolutePath;
237
+ }
238
+ async createDirectory(workspaceId, path) {
239
+ const workspace = this.getWorkspace(workspaceId);
240
+ assertPermission(workspace.exposedPath, "write");
241
+ const absolutePath = this.resolvePath(workspace, path);
242
+ await assertRealPathInside(workspace, await nearestExistingParent(absolutePath), path);
243
+ await mkdir(absolutePath, { recursive: true });
244
+ }
245
+ async deletePath(workspaceId, path, recursive = false) {
246
+ const workspace = this.getWorkspace(workspaceId);
247
+ assertPermission(workspace.exposedPath, "write");
248
+ const absolutePath = this.resolvePath(workspace, path);
249
+ await assertRealPathInside(workspace, absolutePath, path);
250
+ assertNotWorkspaceRoot(workspace, absolutePath, "delete");
251
+ await rm(absolutePath, { recursive, force: false });
252
+ }
253
+ async movePath(workspaceId, fromPath, toPath) {
254
+ const workspace = this.getWorkspace(workspaceId);
255
+ assertPermission(workspace.exposedPath, "write");
256
+ const absoluteFromPath = this.resolvePath(workspace, fromPath);
257
+ const absoluteToPath = await this.resolveWritablePath(workspace, toPath);
258
+ await assertRealPathInside(workspace, absoluteFromPath, fromPath);
259
+ assertNotWorkspaceRoot(workspace, absoluteFromPath, "move");
260
+ await mkdir(dirname(absoluteToPath), { recursive: true });
261
+ await rename(absoluteFromPath, absoluteToPath);
262
+ }
263
+ }
264
+ async function isDirectory(path) {
265
+ try {
266
+ return (await stat(path)).isDirectory();
267
+ }
268
+ catch {
269
+ return false;
270
+ }
271
+ }
272
+ export function formatWorkspacePath(path, workspace) {
273
+ const relationship = relative(workspace.root, path);
274
+ return relationship ? relationship.split(sep).join("/") : ".";
275
+ }
276
+ async function pathInfo(path, workspace) {
277
+ const info = await lstat(path);
278
+ return {
279
+ path: formatWorkspacePath(path, workspace),
280
+ name: basename(path) || ".",
281
+ type: info.isDirectory() ? "directory" : info.isFile() ? "file" : info.isSymbolicLink() ? "symlink" : "other",
282
+ size: info.size,
283
+ modifiedAt: info.mtime.toISOString(),
284
+ };
285
+ }
286
+ function assertNotWorkspaceRoot(workspace, path, action) {
287
+ if (resolve(path) !== resolve(workspace.root))
288
+ return;
289
+ throw new Error(`Refusing to ${action} the workspace root`);
290
+ }
291
+ async function assertRealPathInside(workspace, path, inputPath) {
292
+ const realPath = await realpath(path);
293
+ if (!isPathInsideRoot(realPath, workspace.root) || !isPathInsideRoot(realPath, workspace.exposedPath.path)) {
294
+ throw new Error(`Path resolves outside workspace: ${inputPath}`);
295
+ }
296
+ }
297
+ async function nearestExistingParent(path) {
298
+ let current = path;
299
+ while (!(await pathExists(current))) {
300
+ const parent = dirname(current);
301
+ if (parent === current)
302
+ return current;
303
+ current = parent;
304
+ }
305
+ return current;
306
+ }
307
+ async function pathExists(path) {
308
+ try {
309
+ await lstat(path);
310
+ return true;
311
+ }
312
+ catch (error) {
313
+ if (isNotFoundError(error))
314
+ return false;
315
+ throw error;
316
+ }
317
+ }
318
+ function isNotFoundError(error) {
319
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
320
+ }
321
+ function normalizePositiveInteger(value, fallback, max) {
322
+ return Number.isInteger(value) && value !== undefined && value > 0 ? Math.min(value, max) : fallback;
323
+ }
324
+ async function instructionSearchDirectory(path) {
325
+ try {
326
+ const info = await lstat(path);
327
+ return info.isDirectory() ? path : dirname(path);
328
+ }
329
+ catch {
330
+ return dirname(path);
331
+ }
332
+ }
333
+ function ancestorDirectories(root, directory) {
334
+ const resolvedRoot = resolve(root);
335
+ let current = resolve(directory);
336
+ const directories = [];
337
+ while (isPathInsideRoot(current, resolvedRoot)) {
338
+ directories.push(current);
339
+ if (current === resolvedRoot)
340
+ break;
341
+ current = dirname(current);
342
+ }
343
+ return directories.reverse();
344
+ }
345
+ const SKIPPED_TREE_DIRECTORIES = new Set([
346
+ ".git",
347
+ "node_modules",
348
+ "dist",
349
+ "build",
350
+ ".next",
351
+ ".cache",
352
+ ]);
353
+ const INSTRUCTION_FILE_NAMES = ["AGENTS.md", "CLAUDE.md"];
@@ -0,0 +1,65 @@
1
+ # Agent Instructions
2
+
3
+ Use these instructions when connecting an MCP-capable agent to Workspace
4
+ Linker.
5
+
6
+ ## First Call
7
+
8
+ Call `get_computer_info` before any workspace action. Read:
9
+
10
+ - available scopes
11
+ - workspace paths and names
12
+ - permissions
13
+ - `computerOperationRegistry`
14
+ - local/public MCP URL status
15
+ - safety boundaries
16
+
17
+ ## Normal Flow
18
+
19
+ 1. Choose one scope from `get_computer_info`.
20
+ 2. Call `computer_operation` with the generic envelope:
21
+
22
+ ```json
23
+ {
24
+ "scope": "workspace-id",
25
+ "op": "file.list",
26
+ "target": ".",
27
+ "input": {},
28
+ "options": {}
29
+ }
30
+ ```
31
+
32
+ 3. Use dotted operation names from `computerOperationRegistry`.
33
+ 4. Use write, shell, command, Codex, or screen operations only when the selected
34
+ scope reports the required permission.
35
+ 5. Call `get_operation_history` when debugging recent actions or connection
36
+ behavior.
37
+
38
+ ## Preferred Operations
39
+
40
+ - Inspect files: `file.list`, `file.read`, `file.search`
41
+ - Edit files: `file.write`, `file.edit`, `file.patch`
42
+ - Git inspection: `git.status`, `git.diff`
43
+ - Git mutation: `git.stage`, `git.commit`
44
+ - Package commands: `package.run`
45
+ - Shell command: `command.run`
46
+
47
+ The exact list can grow. Always prefer names returned by
48
+ `computerOperationRegistry`.
49
+
50
+ ## Sensitive Files
51
+
52
+ Do not request secrets unless the user explicitly asks outside Workspace
53
+ Linker. Direct reads and text searches block common sensitive files by default,
54
+ including `.env*`, private keys, credential JSON files, and cloud CLI
55
+ credential folders. Treat missing matches in those files as an intentional
56
+ safety boundary.
57
+
58
+ ## Avoid By Default
59
+
60
+ Do not call compatibility tools such as `list_workspaces`, `open_workspace`, or
61
+ `workspace_operation` unless the MCP client cannot send the generic
62
+ `computer_operation` envelope.
63
+
64
+ Do not assume shell or write access. Computer Linker may expose read-only,
65
+ coding, or full-trust scopes.
@@ -0,0 +1,54 @@
1
+ {
2
+ "kind": "computer-linker-alpha-evidence",
3
+ "schemaVersion": 1,
4
+ "testedAt": "2026-06-24T00:00:00.000Z",
5
+ "package": {
6
+ "name": "computer-linker",
7
+ "version": "0.1.0"
8
+ },
9
+ "git": {
10
+ "head": "replace-with-tested-git-head",
11
+ "shortHead": "replace-head"
12
+ },
13
+ "environment": {
14
+ "platform": "win32",
15
+ "arch": "x64",
16
+ "node": "v22.x"
17
+ },
18
+ "target": {
19
+ "client": "External MCP client",
20
+ "exposure": "openai",
21
+ "tunnelOrUrl": "redacted-tunnel-id-or-public-origin",
22
+ "mcpPath": "/mcp",
23
+ "scope": "app"
24
+ },
25
+ "checks": [
26
+ {
27
+ "id": "external-mcp-tool-flow",
28
+ "status": "pass",
29
+ "evidence": "External MCP client called get_computer_info, computer_operation, and get_operation_history successfully."
30
+ },
31
+ {
32
+ "id": "tunnel-transport",
33
+ "status": "pass",
34
+ "evidence": "Tunnel exposure connected from outside 127.0.0.1 and reached the local MCP server."
35
+ },
36
+ {
37
+ "id": "mcp-only-public-surface",
38
+ "status": "pass",
39
+ "evidence": "Public exposure allowed /mcp and did not expose /api/v1 or /healthz."
40
+ },
41
+ {
42
+ "id": "operation-history-reviewed",
43
+ "status": "pass",
44
+ "evidence": "history --view connections and history --view last showed the expected external MCP session and no secret payloads."
45
+ },
46
+ {
47
+ "id": "client-instructions-usable",
48
+ "status": "pass",
49
+ "evidence": "README Agent Instructions produced the expected first operations in the external client."
50
+ }
51
+ ],
52
+ "redactionConfirmed": true,
53
+ "notes": "Do not store owner tokens, API keys, bearer headers, screenshots, or private file contents in this evidence file."
54
+ }
@@ -0,0 +1,56 @@
1
+ # API Compatibility
2
+
3
+ Computer Linker is still `0.x`, but the public MCP surface is intentionally
4
+ small and treated as the product contract.
5
+
6
+ ## Stable MCP Surface
7
+
8
+ Default MCP clients should use only these tools:
9
+
10
+ - `get_computer_info`
11
+ - `computer_operation`
12
+ - `get_operation_history`
13
+
14
+ The stable `computer_operation` request envelope is:
15
+
16
+ ```json
17
+ {
18
+ "scope": "workspace-id",
19
+ "op": "file.list",
20
+ "target": ".",
21
+ "input": {},
22
+ "options": {}
23
+ }
24
+ ```
25
+
26
+ The stable operation result envelope includes:
27
+
28
+ - `ok`
29
+ - `operationId`
30
+ - `scope`
31
+ - `op`
32
+ - `startedAt`
33
+ - `durationMs`
34
+ - `data`
35
+ - `error`
36
+ - `warnings`
37
+
38
+ New dotted operations, new optional fields, and additional diagnostic metadata
39
+ are non-breaking additions.
40
+
41
+ ## Compatibility Tools
42
+
43
+ Tools such as `list_workspaces`, `open_workspace`, and `workspace_operation`
44
+ exist for older clients and migration. New clients should not depend on them
45
+ unless a specific MCP client cannot use the generic `computer_operation`
46
+ contract.
47
+
48
+ Removing or renaming a default MCP tool, removing required envelope fields, or
49
+ changing operation semantics is a breaking change and must be called out in the
50
+ changelog.
51
+
52
+ ## JSON API
53
+
54
+ The JSON API under `/api/v1` is for local and trusted-private diagnostics,
55
+ SDK usage, and health checks. Public cloud exposure should route only `/mcp`
56
+ unless an operator intentionally exposes more.