@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,108 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { loadConfig } from "./config.js";
3
+ import { executableCommand, windowsVerbatimArgumentsOption } from "./platform-shell.js";
4
+ export function securityDiagnostics(config = loadConfig()) {
5
+ const findings = [];
6
+ const host = config.host ?? "127.0.0.1";
7
+ const ownerTokenConfigured = Boolean(config.ownerToken);
8
+ if (!ownerTokenConfigured) {
9
+ findings.push({
10
+ id: "owner-token-missing",
11
+ severity: isLoopbackHost(host) ? "info" : "critical",
12
+ title: "Owner token is not configured",
13
+ detail: isLoopbackHost(host)
14
+ ? "HTTP mode is loopback-only without an owner token. Configure one before using a tunnel."
15
+ : "A non-loopback HTTP server without an owner token must not be exposed.",
16
+ });
17
+ }
18
+ if (ownerTokenConfigured && !config.publicBaseUrl) {
19
+ findings.push({
20
+ id: "public-base-url-missing",
21
+ severity: "warning",
22
+ title: "Public base URL is not configured",
23
+ detail: "OAuth clients behind Cloudflare or Tailscale need publicBaseUrl to match the reachable origin.",
24
+ });
25
+ }
26
+ if (config.publicBaseUrl && !isHttpsUrl(config.publicBaseUrl)) {
27
+ findings.push({
28
+ id: "public-base-url-not-https",
29
+ severity: "warning",
30
+ title: "Public base URL is not HTTPS",
31
+ detail: "Cloud-hosted MCP clients such as ChatGPT need a reachable HTTPS origin rather than localhost or plain HTTP.",
32
+ });
33
+ }
34
+ if (!isLoopbackHost(host)) {
35
+ findings.push({
36
+ id: "non-loopback-host",
37
+ severity: "warning",
38
+ title: "HTTP server listens beyond loopback",
39
+ detail: `host is ${host}. Use owner token, OAuth, and a network layer such as Tailscale or Cloudflare Access.`,
40
+ });
41
+ }
42
+ for (const workspace of config.workspaces) {
43
+ if (workspace.permissions.shell) {
44
+ findings.push({
45
+ id: "shell-broad-access",
46
+ severity: "warning",
47
+ title: "Shell permission is broad",
48
+ detail: "Computer Linker starts commands in the workspace, but the OS shell itself is not a filesystem sandbox.",
49
+ workspaceId: workspace.id,
50
+ });
51
+ }
52
+ if ((workspace.permissions.shell || workspace.permissions.codex) && !workspace.policy?.allowedCommands?.length) {
53
+ findings.push({
54
+ id: "command-allowlist-missing",
55
+ severity: "warning",
56
+ title: "Command allowlist is missing",
57
+ detail: "This scope allows local execution without an allowedCommands policy. Commands remain cwd-bound, not filesystem-sandboxed.",
58
+ workspaceId: workspace.id,
59
+ });
60
+ }
61
+ if (workspace.permissions.codex) {
62
+ findings.push({
63
+ id: "codex-broad-access",
64
+ severity: commandAvailable("codex") ? "warning" : "critical",
65
+ title: "Codex permission is broad",
66
+ detail: commandAvailable("codex")
67
+ ? "Computer Linker starts codex in the workspace, but codex may invoke tools with broader OS access."
68
+ : "This workspace allows codex, but the codex CLI was not found on PATH.",
69
+ workspaceId: workspace.id,
70
+ });
71
+ }
72
+ }
73
+ if (findings.length === 0) {
74
+ findings.push({
75
+ id: "security-baseline-ok",
76
+ severity: "info",
77
+ title: "No immediate security findings",
78
+ detail: "Workspace access is limited to read/write operations unless shell or codex permissions are enabled.",
79
+ });
80
+ }
81
+ return findings;
82
+ }
83
+ function isLoopbackHost(host) {
84
+ return host === "127.0.0.1" || host === "localhost" || host === "::1";
85
+ }
86
+ function isHttpsUrl(value) {
87
+ try {
88
+ return new URL(value).protocol === "https:";
89
+ }
90
+ catch {
91
+ return false;
92
+ }
93
+ }
94
+ function commandAvailable(command) {
95
+ try {
96
+ const invocation = executableCommand(command, ["--version"]);
97
+ execFileSync(invocation.command, invocation.args, {
98
+ encoding: "utf8",
99
+ timeout: 1500,
100
+ stdio: ["ignore", "pipe", "pipe"],
101
+ ...windowsVerbatimArgumentsOption(invocation),
102
+ });
103
+ return true;
104
+ }
105
+ catch {
106
+ return false;
107
+ }
108
+ }
@@ -0,0 +1,4 @@
1
+ export declare const SENSITIVE_FILE_RG_GLOBS: string[];
2
+ export declare function sensitiveFileRgGlobArgs(): string[];
3
+ export declare function isSensitiveWorkspacePath(path: string): boolean;
4
+ export declare function assertNonSensitiveWorkspacePath(path: string, operation?: string): void;
@@ -0,0 +1,96 @@
1
+ import { basename, dirname } from "node:path";
2
+ const SENSITIVE_DIRECTORY_NAMES = new Set([
3
+ ".aws",
4
+ ".azure",
5
+ ".docker",
6
+ ".gcloud",
7
+ ".gnupg",
8
+ ".ssh",
9
+ ]);
10
+ const SENSITIVE_FILE_NAMES = new Set([
11
+ ".env",
12
+ ".netrc",
13
+ ".npmrc",
14
+ ".pypirc",
15
+ "credentials",
16
+ "credentials.json",
17
+ "id_dsa",
18
+ "id_ecdsa",
19
+ "id_ed25519",
20
+ "id_rsa",
21
+ "secrets.json",
22
+ "service-account.json",
23
+ ]);
24
+ const ALLOWED_EXAMPLE_FILE_NAMES = new Set([
25
+ ".env.example",
26
+ ".env.sample",
27
+ ".env.template",
28
+ ]);
29
+ const SENSITIVE_EXTENSIONS = [
30
+ ".key",
31
+ ".kdbx",
32
+ ".p12",
33
+ ".pem",
34
+ ".pfx",
35
+ ];
36
+ export const SENSITIVE_FILE_RG_GLOBS = [
37
+ "!**/.aws/**",
38
+ "!**/.azure/**",
39
+ "!**/.docker/config.json",
40
+ "!**/.env",
41
+ "!**/.env.*",
42
+ "!**/.gcloud/**",
43
+ "!**/.gnupg/**",
44
+ "!**/.netrc",
45
+ "!**/.npmrc",
46
+ "!**/.pypirc",
47
+ "!**/.ssh/**",
48
+ "!**/*credentials*.json",
49
+ "!**/*secret*.json",
50
+ "!**/*.kdbx",
51
+ "!**/*.key",
52
+ "!**/*.p12",
53
+ "!**/*.pem",
54
+ "!**/*.pfx",
55
+ "!**/id_dsa",
56
+ "!**/id_ecdsa",
57
+ "!**/id_ed25519",
58
+ "!**/id_rsa",
59
+ "!**/service-account*.json",
60
+ ];
61
+ export function sensitiveFileRgGlobArgs() {
62
+ return SENSITIVE_FILE_RG_GLOBS.flatMap((glob) => ["--glob", glob]);
63
+ }
64
+ export function isSensitiveWorkspacePath(path) {
65
+ const portablePath = path.replaceAll("\\", "/");
66
+ const parts = portablePath.split("/").filter(Boolean);
67
+ if (parts.length === 0)
68
+ return false;
69
+ if (parts.some((part) => SENSITIVE_DIRECTORY_NAMES.has(part.toLowerCase()))) {
70
+ const base = parts.at(-1)?.toLowerCase() ?? "";
71
+ if (base === "known_hosts" || base.endsWith(".pub"))
72
+ return false;
73
+ return true;
74
+ }
75
+ const name = basename(portablePath).toLowerCase();
76
+ if (ALLOWED_EXAMPLE_FILE_NAMES.has(name))
77
+ return false;
78
+ if (SENSITIVE_FILE_NAMES.has(name))
79
+ return true;
80
+ if (name.startsWith(".env."))
81
+ return true;
82
+ if (SENSITIVE_EXTENSIONS.some((extension) => name.endsWith(extension)))
83
+ return true;
84
+ if (/(^|[-_.])(secret|secrets|credential|credentials)([-_.]|$)/i.test(name))
85
+ return true;
86
+ if (/^service-account[-_.].*\.json$/i.test(name))
87
+ return true;
88
+ const parent = basename(dirname(portablePath)).toLowerCase();
89
+ return parent === ".docker" && name === "config.json";
90
+ }
91
+ export function assertNonSensitiveWorkspacePath(path, operation = "read") {
92
+ if (!isSensitiveWorkspacePath(path))
93
+ return;
94
+ throw new Error(`Sensitive file ${operation} is blocked by default: ${path}. ` +
95
+ "Move secrets outside the workspace before exposing it to an MCP client.");
96
+ }
@@ -0,0 +1,9 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare function createLocalPortMcpServer(): McpServer;
3
+ export declare function serveStdio(): Promise<void>;
4
+ export declare function serveHttp(): {
5
+ url: string;
6
+ publicUrl: string;
7
+ apiUrl: string;
8
+ close(): void;
9
+ };