@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,11 @@
1
+ import type { LocalPortConfig } from "./permissions.js";
2
+ export type ConfigDiagnosticSeverity = "info" | "warning" | "critical";
3
+ export interface ConfigDiagnostic {
4
+ id: string;
5
+ severity: ConfigDiagnosticSeverity;
6
+ title: string;
7
+ detail: string;
8
+ workspaceId?: string;
9
+ path?: string;
10
+ }
11
+ export declare function configDiagnostics(config: LocalPortConfig): ConfigDiagnostic[];
@@ -0,0 +1,185 @@
1
+ import { existsSync, statSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ export function configDiagnostics(config) {
4
+ const findings = [];
5
+ const workspaceIds = new Set();
6
+ const workspacePaths = new Map();
7
+ if (!config.machineName?.trim()) {
8
+ findings.push({
9
+ id: "machine-name-missing",
10
+ severity: "critical",
11
+ title: "Machine name is missing",
12
+ detail: "machineName is required so clients can identify this computer.",
13
+ });
14
+ }
15
+ if (config.publicBaseUrl && !validUrl(config.publicBaseUrl)) {
16
+ findings.push({
17
+ id: "public-base-url-invalid",
18
+ severity: "critical",
19
+ title: "Public base URL is invalid",
20
+ detail: "publicBaseUrl must be a valid URL origin.",
21
+ });
22
+ }
23
+ if (config.workspaces.length === 0) {
24
+ findings.push({
25
+ id: "workspace-missing",
26
+ severity: "critical",
27
+ title: "No workspace scopes are configured",
28
+ detail: "At least one folder-backed scope is required before clients can operate this computer.",
29
+ });
30
+ }
31
+ for (const workspace of config.workspaces) {
32
+ const workspaceId = workspace.id?.trim();
33
+ if (!workspaceId) {
34
+ findings.push({
35
+ id: "workspace-id-missing",
36
+ severity: "critical",
37
+ title: "Workspace id is missing",
38
+ detail: "Each configured scope needs a stable id.",
39
+ path: workspace.path,
40
+ });
41
+ }
42
+ else if (workspaceIds.has(workspaceId)) {
43
+ findings.push({
44
+ id: "workspace-id-duplicate",
45
+ severity: "critical",
46
+ title: "Workspace id is duplicated",
47
+ detail: `More than one configured scope uses id ${workspaceId}.`,
48
+ workspaceId,
49
+ path: workspace.path,
50
+ });
51
+ }
52
+ else {
53
+ workspaceIds.add(workspaceId);
54
+ }
55
+ const pathFinding = workspacePathFinding(workspace.path, workspaceId);
56
+ if (pathFinding)
57
+ findings.push(pathFinding);
58
+ const pathKey = normalizedWorkspacePathKey(workspace.path);
59
+ if (pathKey) {
60
+ const first = workspacePaths.get(pathKey);
61
+ if (first) {
62
+ findings.push({
63
+ id: "workspace-path-duplicate",
64
+ severity: "warning",
65
+ title: "Workspace path is duplicated",
66
+ detail: `This scope points at the same folder as ${first.workspaceId ?? first.path}. Keep one scope per folder unless separate permissions are intentional.`,
67
+ workspaceId,
68
+ path: workspace.path,
69
+ });
70
+ }
71
+ else {
72
+ workspacePaths.set(pathKey, { workspaceId, path: workspace.path });
73
+ }
74
+ }
75
+ if (!workspace.permissions.read && !workspace.permissions.write && !workspace.permissions.shell && !workspace.permissions.codex && !workspace.permissions.screen) {
76
+ findings.push({
77
+ id: "workspace-no-permissions",
78
+ severity: "warning",
79
+ title: "Workspace has no enabled permissions",
80
+ detail: "This scope is visible to clients but has no useful operation permissions.",
81
+ workspaceId,
82
+ path: workspace.path,
83
+ });
84
+ }
85
+ if (workspace.permissions.write && !workspace.permissions.read) {
86
+ findings.push({
87
+ id: "workspace-write-without-read",
88
+ severity: "warning",
89
+ title: "Write permission is enabled without read",
90
+ detail: "Most editing workflows need read permission to verify current content before writing.",
91
+ workspaceId,
92
+ path: workspace.path,
93
+ });
94
+ }
95
+ if ((workspace.permissions.shell || workspace.permissions.codex) && !workspace.policy) {
96
+ findings.push({
97
+ id: "workspace-execution-policy-missing",
98
+ severity: "warning",
99
+ title: "Execution policy is missing",
100
+ detail: "Shell or Codex scopes should set allowedCommands, maxRuntimeSeconds, and maxOutputBytes before broad use.",
101
+ workspaceId,
102
+ path: workspace.path,
103
+ });
104
+ }
105
+ else if ((workspace.permissions.shell || workspace.permissions.codex) && !workspace.policy?.allowedCommands?.length) {
106
+ findings.push({
107
+ id: "workspace-command-allowlist-missing",
108
+ severity: "warning",
109
+ title: "Command allowlist is missing",
110
+ detail: "Without allowedCommands, any command can run from this scope when shell or Codex permission is enabled.",
111
+ workspaceId,
112
+ path: workspace.path,
113
+ });
114
+ }
115
+ }
116
+ if (findings.length === 0) {
117
+ findings.push({
118
+ id: "config-baseline-ok",
119
+ severity: "info",
120
+ title: "Configuration baseline is valid",
121
+ detail: "Configured scopes are usable and no immediate config issues were found.",
122
+ });
123
+ }
124
+ return findings;
125
+ }
126
+ function normalizedWorkspacePathKey(path) {
127
+ const text = path?.trim();
128
+ if (!text)
129
+ return undefined;
130
+ const resolved = resolve(text);
131
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
132
+ }
133
+ function workspacePathFinding(path, workspaceId) {
134
+ if (!path?.trim()) {
135
+ return {
136
+ id: "workspace-path-missing",
137
+ severity: "critical",
138
+ title: "Workspace path is missing",
139
+ detail: "Folder-backed scopes require a path.",
140
+ workspaceId,
141
+ };
142
+ }
143
+ if (!existsSync(path)) {
144
+ return {
145
+ id: "workspace-path-missing-on-disk",
146
+ severity: "critical",
147
+ title: "Workspace path does not exist",
148
+ detail: "Operations cannot run until the configured path exists.",
149
+ workspaceId,
150
+ path,
151
+ };
152
+ }
153
+ try {
154
+ if (!statSync(path).isDirectory()) {
155
+ return {
156
+ id: "workspace-path-not-directory",
157
+ severity: "critical",
158
+ title: "Workspace path is not a directory",
159
+ detail: "Folder-backed scopes must point at a directory.",
160
+ workspaceId,
161
+ path,
162
+ };
163
+ }
164
+ }
165
+ catch (error) {
166
+ return {
167
+ id: "workspace-path-unreadable",
168
+ severity: "critical",
169
+ title: "Workspace path cannot be inspected",
170
+ detail: error instanceof Error ? error.message : String(error),
171
+ workspaceId,
172
+ path,
173
+ };
174
+ }
175
+ return undefined;
176
+ }
177
+ function validUrl(value) {
178
+ try {
179
+ void new URL(value);
180
+ return true;
181
+ }
182
+ catch {
183
+ return false;
184
+ }
185
+ }
@@ -0,0 +1,10 @@
1
+ import { type LocalPortConfig } from "./permissions.js";
2
+ export declare function configDir(): string;
3
+ export declare function configPath(): string;
4
+ export declare function auditLogPath(): string;
5
+ export declare function codexRunsPath(): string;
6
+ export declare function oauthStatePath(): string;
7
+ export declare function loadConfig(): LocalPortConfig;
8
+ export declare function writeDefaultConfig(): string;
9
+ export declare function writeConfig(config: LocalPortConfig): string;
10
+ export declare function generateOwnerToken(): string;
package/dist/config.js ADDED
@@ -0,0 +1,69 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join, resolve } from "node:path";
5
+ import { defaultConfig, expandHomePath, normalizeConfig, } from "./permissions.js";
6
+ export function configDir() {
7
+ return resolve(expandHomePath(firstNonBlank(process.env.COMPUTER_LINKER_CONFIG_DIR, process.env.WORKSPACE_LINKER_CONFIG_DIR, process.env.LOCALPORT_CONFIG_DIR) ?? join(homedir(), ".computer-linker")));
8
+ }
9
+ export function configPath() {
10
+ return join(configDir(), "config.json");
11
+ }
12
+ export function auditLogPath() {
13
+ return join(configDir(), "audit.jsonl");
14
+ }
15
+ export function codexRunsPath() {
16
+ return join(configDir(), "codex-runs.jsonl");
17
+ }
18
+ export function oauthStatePath() {
19
+ return join(configDir(), "oauth-state.json");
20
+ }
21
+ export function loadConfig() {
22
+ const path = configPath();
23
+ if (!existsSync(path)) {
24
+ writeConfig(defaultConfig());
25
+ }
26
+ try {
27
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
28
+ const normalized = normalizeConfig(withEnvOverrides(parsed));
29
+ if (!parsed.machineId?.trim()) {
30
+ writeConfig({
31
+ ...parsed,
32
+ machineId: normalized.machineId,
33
+ });
34
+ }
35
+ return normalized;
36
+ }
37
+ catch (error) {
38
+ const reason = error instanceof Error ? error.message : String(error);
39
+ throw new Error(`Unable to read ${path}: ${reason}`);
40
+ }
41
+ }
42
+ export function writeDefaultConfig() {
43
+ const path = configPath();
44
+ mkdirSync(configDir(), { recursive: true });
45
+ writeFileSync(path, JSON.stringify({
46
+ ...defaultConfig(),
47
+ ownerToken: generateOwnerToken(),
48
+ }, null, 2) + "\n", { mode: 0o600 });
49
+ return path;
50
+ }
51
+ export function writeConfig(config) {
52
+ const path = configPath();
53
+ mkdirSync(configDir(), { recursive: true });
54
+ writeFileSync(path, JSON.stringify(normalizeConfig(config), null, 2) + "\n", { mode: 0o600 });
55
+ return path;
56
+ }
57
+ export function generateOwnerToken() {
58
+ return randomBytes(32).toString("base64url");
59
+ }
60
+ function withEnvOverrides(config) {
61
+ return {
62
+ ...config,
63
+ publicBaseUrl: firstNonBlank(process.env.COMPUTER_LINKER_PUBLIC_BASE_URL, process.env.WORKSPACE_LINKER_PUBLIC_BASE_URL, process.env.LOCALPORT_PUBLIC_BASE_URL) ?? config.publicBaseUrl,
64
+ ownerToken: firstNonBlank(process.env.COMPUTER_LINKER_OWNER_TOKEN, process.env.WORKSPACE_LINKER_OWNER_TOKEN, process.env.LOCALPORT_OWNER_TOKEN) ?? config.ownerToken,
65
+ };
66
+ }
67
+ function firstNonBlank(...values) {
68
+ return values.find((value) => value !== undefined && value.trim().length > 0);
69
+ }
@@ -0,0 +1,132 @@
1
+ import { type AuditEvent, type AuditReplayTemplate } from "./audit.js";
2
+ export type HistoryInsightView = "summary" | "last" | "timeline" | "sessions" | "connections" | "failed_replay" | "debug_bundle";
3
+ export interface HistoryInsightOptions {
4
+ view?: string;
5
+ limit?: number;
6
+ query?: string;
7
+ workspaceId?: string;
8
+ events?: AuditEvent[];
9
+ }
10
+ export interface CompactAuditEvent {
11
+ timestamp: string;
12
+ type: AuditEvent["type"];
13
+ success: boolean;
14
+ tool?: string;
15
+ workspaceId?: string;
16
+ workspaceRef?: string;
17
+ requestPath?: string;
18
+ remoteAddress?: string;
19
+ path?: string;
20
+ workingDirectory?: string;
21
+ commandPreview?: string;
22
+ operation?: string;
23
+ target?: string;
24
+ detail?: string;
25
+ replay?: AuditReplayTemplate;
26
+ error?: string;
27
+ durationMs?: number;
28
+ provider?: string;
29
+ tunnelId?: string;
30
+ externalSessionId?: string;
31
+ requestId?: string;
32
+ cmdRequestId?: string;
33
+ rpcRequestId?: string;
34
+ tunnelRequestId?: string;
35
+ severity?: "info" | "warn" | "error";
36
+ statusCode?: number;
37
+ }
38
+ export interface FailedReplayItem {
39
+ timestamp: string;
40
+ error?: string;
41
+ replayable: boolean;
42
+ reason?: string;
43
+ requiresInput?: string[];
44
+ request?: {
45
+ action: "workspace_operation";
46
+ workspace: string;
47
+ input: {
48
+ op: string;
49
+ target?: string;
50
+ input: Record<string, unknown>;
51
+ options: Record<string, unknown>;
52
+ };
53
+ };
54
+ }
55
+ export interface HistorySessionSummary {
56
+ key: string;
57
+ scope: "workspace" | "surface";
58
+ workspaceId?: string;
59
+ workspaceRef?: string;
60
+ surface?: string;
61
+ startedAt: string;
62
+ lastActivityAt: string;
63
+ totalEvents: number;
64
+ successfulEvents: number;
65
+ failedEvents: number;
66
+ tools: Record<string, number>;
67
+ operations: Record<string, number>;
68
+ lastEvent: CompactAuditEvent;
69
+ recentFailures: CompactAuditEvent[];
70
+ }
71
+ export interface HistoryConnectionSummary {
72
+ key: string;
73
+ scope: "tunnel" | "mcp" | "workspace" | "surface";
74
+ provider?: string;
75
+ tunnelId?: string;
76
+ externalSessionId?: string;
77
+ remoteAddress?: string;
78
+ startedAt: string;
79
+ lastActivityAt: string;
80
+ totalEvents: number;
81
+ successfulEvents: number;
82
+ failedEvents: number;
83
+ requestCount: number;
84
+ tools: Record<string, number>;
85
+ operations: Record<string, number>;
86
+ lastEvent: CompactAuditEvent;
87
+ recentFailures: CompactAuditEvent[];
88
+ }
89
+ export interface HistoryLastInsight {
90
+ event?: CompactAuditEvent;
91
+ workspaceOperation?: CompactAuditEvent;
92
+ failure?: CompactAuditEvent;
93
+ replay?: FailedReplayItem;
94
+ session?: HistorySessionSummary;
95
+ connection?: HistoryConnectionSummary;
96
+ suggestedNextActions: string[];
97
+ }
98
+ export interface HistoryInsight {
99
+ view: HistoryInsightView;
100
+ generatedAt: string;
101
+ filters: {
102
+ workspaceId?: string;
103
+ query?: string;
104
+ limit: number;
105
+ };
106
+ summary: {
107
+ totalEvents: number;
108
+ successfulEvents: number;
109
+ failedEvents: number;
110
+ lastEvent?: CompactAuditEvent;
111
+ lastWorkspaceOperation?: CompactAuditEvent;
112
+ recentFailures: CompactAuditEvent[];
113
+ toolCounts: Record<string, number>;
114
+ workspaceCounts: Record<string, number>;
115
+ };
116
+ last?: HistoryLastInsight;
117
+ timeline?: CompactAuditEvent[];
118
+ sessions?: HistorySessionSummary[];
119
+ connections?: HistoryConnectionSummary[];
120
+ failedReplay?: FailedReplayItem[];
121
+ debugBundle?: {
122
+ format: "computer-linker-debug-bundle-v1";
123
+ auditLogPath: string;
124
+ redactions: string[];
125
+ events: CompactAuditEvent[];
126
+ connections: HistoryConnectionSummary[];
127
+ failedReplay: FailedReplayItem[];
128
+ };
129
+ }
130
+ export declare function historyInsight(options?: HistoryInsightOptions): HistoryInsight;
131
+ export declare function historyInsightFromEvents(events: AuditEvent[], options?: HistoryInsightOptions): HistoryInsight;
132
+ export declare function historyInsightView(value: string | undefined): HistoryInsightView;