@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.
- package/CHANGELOG.md +230 -0
- package/LICENSE +21 -0
- package/README.md +539 -0
- package/SECURITY.md +48 -0
- package/dist/api.d.ts +2 -0
- package/dist/api.js +360 -0
- package/dist/audit.d.ts +70 -0
- package/dist/audit.js +102 -0
- package/dist/capabilities.d.ts +98 -0
- package/dist/capabilities.js +718 -0
- package/dist/capability-policy.d.ts +22 -0
- package/dist/capability-policy.js +103 -0
- package/dist/chatgpt.d.ts +167 -0
- package/dist/chatgpt.js +561 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +4621 -0
- package/dist/client-smoke.d.ts +44 -0
- package/dist/client-smoke.js +639 -0
- package/dist/client.d.ts +217 -0
- package/dist/client.js +357 -0
- package/dist/codex-runs.d.ts +35 -0
- package/dist/codex-runs.js +66 -0
- package/dist/computer-contract.d.ts +33 -0
- package/dist/computer-contract.js +384 -0
- package/dist/computer-operation-registry.d.ts +45 -0
- package/dist/computer-operation-registry.js +179 -0
- package/dist/config-diagnostics.d.ts +11 -0
- package/dist/config-diagnostics.js +185 -0
- package/dist/config.d.ts +10 -0
- package/dist/config.js +69 -0
- package/dist/history-insights.d.ts +132 -0
- package/dist/history-insights.js +457 -0
- package/dist/http-auth.d.ts +3 -0
- package/dist/http-auth.js +15 -0
- package/dist/mcp-surface.d.ts +5 -0
- package/dist/mcp-surface.js +25 -0
- package/dist/oauth-provider.d.ts +52 -0
- package/dist/oauth-provider.js +325 -0
- package/dist/package-metadata.d.ts +7 -0
- package/dist/package-metadata.js +24 -0
- package/dist/permissions.d.ts +43 -0
- package/dist/permissions.js +150 -0
- package/dist/platform-shell.d.ts +28 -0
- package/dist/platform-shell.js +124 -0
- package/dist/processes.d.ts +50 -0
- package/dist/processes.js +178 -0
- package/dist/profile.d.ts +159 -0
- package/dist/profile.js +416 -0
- package/dist/screenshot.d.ts +47 -0
- package/dist/screenshot.js +302 -0
- package/dist/search.d.ts +34 -0
- package/dist/search.js +340 -0
- package/dist/security.d.ts +10 -0
- package/dist/security.js +108 -0
- package/dist/sensitive-files.d.ts +4 -0
- package/dist/sensitive-files.js +96 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +713 -0
- package/dist/service.d.ts +125 -0
- package/dist/service.js +486 -0
- package/dist/sessions.d.ts +26 -0
- package/dist/sessions.js +34 -0
- package/dist/tunnels.d.ts +161 -0
- package/dist/tunnels.js +1243 -0
- package/dist/workspace-operations.d.ts +170 -0
- package/dist/workspace-operations.js +3219 -0
- package/dist/workspaces.d.ts +61 -0
- package/dist/workspaces.js +353 -0
- package/docs/agent-instructions.md +65 -0
- package/docs/alpha-evidence.example.json +54 -0
- package/docs/api-compatibility.md +56 -0
- package/docs/architecture.md +561 -0
- package/docs/chatgpt-setup.md +397 -0
- package/docs/client-recipes.md +98 -0
- package/docs/client-sdk.md +163 -0
- package/docs/computer-operation-v1.schema.json +143 -0
- package/docs/manual-test-plan.md +322 -0
- package/docs/product-spec.md +911 -0
- package/docs/release-checklist.md +285 -0
- package/docs/service-mode.md +99 -0
- package/examples/minimal-mcp-client.mjs +114 -0
- package/package.json +87 -0
package/dist/chatgpt.js
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
import { securityDiagnostics } from "./security.js";
|
|
2
|
+
import { listTunnelProcesses, tunnelDiagnostics } from "./tunnels.js";
|
|
3
|
+
import { chatGptConnectProfile, parseChatGptProfileMode } from "./profile.js";
|
|
4
|
+
import { runWorkspaceLinkerMcpClientSmoke } from "./client-smoke.js";
|
|
5
|
+
import { genericMcpTools } from "./mcp-surface.js";
|
|
6
|
+
const CHATGPT_TOOLS = [...genericMcpTools];
|
|
7
|
+
export function chatGptVerify(config, mode = "safe") {
|
|
8
|
+
const profile = chatGptConnectProfile(config, false, mode);
|
|
9
|
+
const checks = [];
|
|
10
|
+
const security = securityDiagnostics(config);
|
|
11
|
+
const tunnel = tunnelDiagnostics({
|
|
12
|
+
localPort: config.port ?? 3939,
|
|
13
|
+
publicBaseUrl: config.publicBaseUrl,
|
|
14
|
+
tunnels: listTunnelProcesses(),
|
|
15
|
+
});
|
|
16
|
+
checks.push(publicBaseUrlCheck(config.publicBaseUrl));
|
|
17
|
+
checks.push(mcpUrlCheck(profile.mcpServerUrl));
|
|
18
|
+
checks.push(ownerTokenCheck(config));
|
|
19
|
+
checks.push(workspaceCheck(config));
|
|
20
|
+
checks.push(toolSurfaceCheck(profile.tools));
|
|
21
|
+
checks.push(modePermissionCheck(config, mode));
|
|
22
|
+
for (const finding of security) {
|
|
23
|
+
checks.push({
|
|
24
|
+
id: `security:${finding.id}`,
|
|
25
|
+
status: finding.severity === "critical" ? "fail" : "warn",
|
|
26
|
+
message: finding.title,
|
|
27
|
+
detail: finding.workspaceId ? `${finding.workspaceId}: ${finding.detail}` : finding.detail,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
if (!config.publicBaseUrl && tunnel.tools.every((tool) => !tool.available)) {
|
|
31
|
+
checks.push({
|
|
32
|
+
id: "tunnel-tool",
|
|
33
|
+
status: "warn",
|
|
34
|
+
message: "No built-in tunnel provider was detected.",
|
|
35
|
+
detail: "Install cloudflared or tailscale, use another HTTPS reverse proxy, or use OpenAI Secure MCP Tunnel from ChatGPT connector settings.",
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
const blockingReasons = checks
|
|
39
|
+
.filter((check) => check.status === "fail")
|
|
40
|
+
.map((check) => `${check.id}: ${check.message}`);
|
|
41
|
+
const warnings = checks
|
|
42
|
+
.filter((check) => check.status === "warn")
|
|
43
|
+
.map((check) => `${check.id}: ${check.message}`);
|
|
44
|
+
return {
|
|
45
|
+
kind: "chatgpt-verify",
|
|
46
|
+
schemaVersion: 1,
|
|
47
|
+
mode,
|
|
48
|
+
ready: blockingReasons.length === 0,
|
|
49
|
+
mcpServerUrl: profile.mcpServerUrl,
|
|
50
|
+
publicBaseUrl: config.publicBaseUrl ?? null,
|
|
51
|
+
authMode: config.ownerToken ? "owner-token-or-oauth" : "loopback-only",
|
|
52
|
+
tools: profile.tools,
|
|
53
|
+
checks,
|
|
54
|
+
blockingReasons,
|
|
55
|
+
warnings,
|
|
56
|
+
nextActions: chatGptNextActions(blockingReasons, warnings, mode),
|
|
57
|
+
recommendedProfileCommand: "computer-linker client chatgpt profile --show-token",
|
|
58
|
+
recommendedSmokeTest: [
|
|
59
|
+
"get_computer_info",
|
|
60
|
+
"computer_operation op=code.context",
|
|
61
|
+
"get_operation_history view=last",
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export function chatGptSetupStatus(config, mode = "coding", options = {}) {
|
|
66
|
+
const detectedPublicUrl = runningTunnelPublicUrl(options.tunnels);
|
|
67
|
+
const effectivePublicBaseUrl = chatGptPublicBaseUrl(config, options.tunnels);
|
|
68
|
+
const effectiveConfig = effectivePublicBaseUrl ? { ...config, publicBaseUrl: effectivePublicBaseUrl } : config;
|
|
69
|
+
const verify = chatGptVerify(effectiveConfig, mode);
|
|
70
|
+
const profile = chatGptConnectProfile(effectiveConfig, false, mode);
|
|
71
|
+
const effectiveMcpServerUrl = chatGptMcpServerUrl(config, options.tunnels) ?? verify.mcpServerUrl;
|
|
72
|
+
const origin = urlOrigin(effectiveMcpServerUrl);
|
|
73
|
+
const configuredOrigin = config.publicBaseUrl ? urlOrigin(new URL("/mcp", config.publicBaseUrl).href) : null;
|
|
74
|
+
const oauthEnabled = Boolean(config.ownerToken && config.publicBaseUrl && origin && origin === configuredOrigin);
|
|
75
|
+
const warnings = [...verify.warnings];
|
|
76
|
+
if (detectedPublicUrl && detectedPublicUrl !== config.publicBaseUrl) {
|
|
77
|
+
warnings.push("detected tunnel URL is used for this setup only; save it as publicBaseUrl before relying on OAuth discovery.");
|
|
78
|
+
}
|
|
79
|
+
const publicUrlArg = effectivePublicBaseUrl ? ` --url ${effectivePublicBaseUrl}` : "";
|
|
80
|
+
const localBaseUrl = `http://${config.host ?? "127.0.0.1"}:${config.port ?? 3939}`;
|
|
81
|
+
const cliCommands = {
|
|
82
|
+
verify: `computer-linker client chatgpt verify --mode ${mode}`,
|
|
83
|
+
profile: `computer-linker client chatgpt profile --mode ${mode}${publicUrlArg} --show-token`,
|
|
84
|
+
manifest: `computer-linker client chatgpt manifest --mode ${mode}${publicUrlArg}`,
|
|
85
|
+
connectorConfig: `computer-linker client chatgpt connector --mode ${mode}${publicUrlArg} --show-token`,
|
|
86
|
+
files: `computer-linker client chatgpt files ./chatgpt-config --mode ${mode}${publicUrlArg} --show-token`,
|
|
87
|
+
localSmoke: `computer-linker client chatgpt smoke --allow-http --url ${localBaseUrl}`,
|
|
88
|
+
publicSmoke: effectivePublicBaseUrl ? `computer-linker client chatgpt smoke --url ${effectivePublicBaseUrl}` : null,
|
|
89
|
+
};
|
|
90
|
+
const setupFields = {
|
|
91
|
+
appName: profile.appManifest.appName,
|
|
92
|
+
connectionType: profile.setup.connectionType,
|
|
93
|
+
mcpServerUrl: effectiveMcpServerUrl,
|
|
94
|
+
authType: profile.appManifest.authType,
|
|
95
|
+
bearerHeader: profile.auth.bearer.header,
|
|
96
|
+
alternateBearerHeader: profile.auth.bearer.alternateHeader,
|
|
97
|
+
};
|
|
98
|
+
const oauthDiscovery = {
|
|
99
|
+
enabled: oauthEnabled,
|
|
100
|
+
issuer: oauthEnabled ? new URL("/", origin).href : null,
|
|
101
|
+
authorizationServerMetadataUrl: oauthEnabled ? new URL("/.well-known/oauth-authorization-server", origin).href : null,
|
|
102
|
+
protectedResourceMetadataUrl: oauthEnabled ? new URL("/.well-known/oauth-protected-resource/mcp", origin).href : null,
|
|
103
|
+
resource: oauthEnabled ? effectiveMcpServerUrl : null,
|
|
104
|
+
scopes: profile.auth.oauth.scopes,
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
kind: "chatgpt-setup-status",
|
|
108
|
+
schemaVersion: 1,
|
|
109
|
+
mode,
|
|
110
|
+
ready: verify.ready,
|
|
111
|
+
mcpServerUrl: effectiveMcpServerUrl,
|
|
112
|
+
publicBaseUrl: verify.publicBaseUrl,
|
|
113
|
+
authMode: verify.authMode,
|
|
114
|
+
setupFields,
|
|
115
|
+
oauthDiscovery,
|
|
116
|
+
smoke: {
|
|
117
|
+
localCli: cliCommands.localSmoke,
|
|
118
|
+
publicCli: cliCommands.publicSmoke,
|
|
119
|
+
},
|
|
120
|
+
cli: cliCommands,
|
|
121
|
+
connectProfile: {
|
|
122
|
+
appName: setupFields.appName,
|
|
123
|
+
mode,
|
|
124
|
+
connectionType: setupFields.connectionType,
|
|
125
|
+
serverUrl: effectiveMcpServerUrl,
|
|
126
|
+
auth: {
|
|
127
|
+
type: setupFields.authType,
|
|
128
|
+
bearerHeader: setupFields.bearerHeader,
|
|
129
|
+
bearerTokenValue: config.ownerToken ? "<ownerToken>" : null,
|
|
130
|
+
bearerTokenSource: config.ownerToken ? "owner-token-config" : null,
|
|
131
|
+
oauthEnabled,
|
|
132
|
+
oauthScopes: oauthDiscovery.scopes,
|
|
133
|
+
oauthAuthorizationServerMetadataUrl: oauthDiscovery.authorizationServerMetadataUrl,
|
|
134
|
+
oauthProtectedResourceMetadataUrl: oauthDiscovery.protectedResourceMetadataUrl,
|
|
135
|
+
},
|
|
136
|
+
ready: verify.ready,
|
|
137
|
+
blockingReasons: verify.blockingReasons,
|
|
138
|
+
warnings,
|
|
139
|
+
nextActions: verify.nextActions,
|
|
140
|
+
firstPrompt: profile.setup.firstPrompt,
|
|
141
|
+
cli: cliCommands,
|
|
142
|
+
},
|
|
143
|
+
checks: verify.checks,
|
|
144
|
+
blockingReasons: verify.blockingReasons,
|
|
145
|
+
warnings,
|
|
146
|
+
nextActions: verify.nextActions,
|
|
147
|
+
wizard: chatGptSetupWizard(config, verify, {
|
|
148
|
+
detectedPublicUrl,
|
|
149
|
+
effectiveMcpServerUrl,
|
|
150
|
+
oauthEnabled,
|
|
151
|
+
}),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
export function parseChatGptVerifyMode(value) {
|
|
155
|
+
return parseChatGptProfileMode(value ?? "safe", "client chatgpt verify --mode");
|
|
156
|
+
}
|
|
157
|
+
export function chatGptPublicBaseUrl(config, tunnels = []) {
|
|
158
|
+
return tunnels
|
|
159
|
+
.filter((tp) => tp.status === "running")
|
|
160
|
+
.map((tp) => tp.publicUrl)
|
|
161
|
+
.find((url) => Boolean(url)) ?? config.publicBaseUrl;
|
|
162
|
+
}
|
|
163
|
+
export function chatGptMcpServerUrl(config, tunnels = []) {
|
|
164
|
+
const publicOrigin = chatGptPublicBaseUrl(config, tunnels);
|
|
165
|
+
return publicOrigin ? new URL("/mcp", publicOrigin).href : undefined;
|
|
166
|
+
}
|
|
167
|
+
function runningTunnelPublicUrl(tunnels) {
|
|
168
|
+
return tunnels
|
|
169
|
+
?.filter((tp) => tp.status === "running")
|
|
170
|
+
.map((tp) => tp.publicUrl)
|
|
171
|
+
.find((url) => Boolean(url)) ?? null;
|
|
172
|
+
}
|
|
173
|
+
function chatGptSetupWizard(config, verify, urls) {
|
|
174
|
+
const hasOwnerToken = Boolean(config.ownerToken);
|
|
175
|
+
const hasConfiguredPublicUrl = Boolean(config.publicBaseUrl);
|
|
176
|
+
const configuredPublicUrlIsHttps = Boolean(config.publicBaseUrl?.startsWith("https://"));
|
|
177
|
+
const effectivePublicUrlIsHttps = Boolean(urls.effectiveMcpServerUrl?.startsWith("https://"));
|
|
178
|
+
const effectiveMcpIsHttps = Boolean(urls.effectiveMcpServerUrl?.startsWith("https://"));
|
|
179
|
+
const hasWorkspace = config.workspaces.length > 0;
|
|
180
|
+
const oauthReady = urls.oauthEnabled;
|
|
181
|
+
const steps = [
|
|
182
|
+
{
|
|
183
|
+
id: "owner_token",
|
|
184
|
+
label: "Owner token",
|
|
185
|
+
status: hasOwnerToken ? "complete" : "blocked",
|
|
186
|
+
detail: hasOwnerToken ? "Owner token is configured for bearer/OAuth access." : "Owner token is required before exposing this machine.",
|
|
187
|
+
action: hasOwnerToken ? undefined : "Run `computer-linker init`.",
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: "public_url",
|
|
191
|
+
label: "Public HTTPS URL",
|
|
192
|
+
status: effectivePublicUrlIsHttps ? "complete" : urls.detectedPublicUrl ? "current" : hasOwnerToken ? "blocked" : "pending",
|
|
193
|
+
detail: configuredPublicUrlIsHttps
|
|
194
|
+
? `Configured public URL: ${config.publicBaseUrl}`
|
|
195
|
+
: urls.detectedPublicUrl
|
|
196
|
+
? `Using detected tunnel URL: ${urls.detectedPublicUrl}. Save it as publicBaseUrl for OAuth and stable reuse.`
|
|
197
|
+
: "No configured public HTTPS URL was found.",
|
|
198
|
+
action: configuredPublicUrlIsHttps
|
|
199
|
+
? undefined
|
|
200
|
+
: urls.detectedPublicUrl
|
|
201
|
+
? `Optional: run \`computer-linker config set-public-url ${urls.detectedPublicUrl}\` to save it for OAuth and stable reuse.`
|
|
202
|
+
: "For first setup, run `computer-linker start <workspace-path> --tunnel tailscale`; for Cloudflare/custom hostnames, pass `--url https://... --tunnel cloudflare`; for ChatGPT Tunnel mode, use `computer-linker start <workspace-path> --tunnel openai --tunnel-id tunnel_...`.",
|
|
203
|
+
},
|
|
204
|
+
{
|
|
205
|
+
id: "mcp_url",
|
|
206
|
+
label: "MCP server URL",
|
|
207
|
+
status: effectiveMcpIsHttps ? "complete" : hasConfiguredPublicUrl ? "blocked" : "pending",
|
|
208
|
+
detail: urls.effectiveMcpServerUrl ? `MCP URL: ${urls.effectiveMcpServerUrl}` : "No MCP URL is available yet.",
|
|
209
|
+
action: effectiveMcpIsHttps ? undefined : "Use an HTTPS tunnel origin with `/mcp`.",
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
id: "oauth",
|
|
213
|
+
label: "OAuth metadata",
|
|
214
|
+
status: oauthReady ? "complete" : hasOwnerToken && configuredPublicUrlIsHttps ? "blocked" : "pending",
|
|
215
|
+
detail: oauthReady ? "OAuth discovery metadata is available from the public origin." : "OAuth metadata needs both owner token and saved publicBaseUrl. Bearer auth can still use a detected tunnel URL.",
|
|
216
|
+
action: oauthReady ? undefined : "Configure owner token and publicBaseUrl before using OAuth discovery.",
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
id: "workspace",
|
|
220
|
+
label: "Workspace boundary",
|
|
221
|
+
status: hasWorkspace ? "complete" : "blocked",
|
|
222
|
+
detail: hasWorkspace ? `${config.workspaces.length} predefined workspace(s) configured.` : "At least one predefined workspace is required.",
|
|
223
|
+
action: hasWorkspace ? undefined : "Run `computer-linker setup <workspace-path>`.",
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "ready",
|
|
227
|
+
label: "Ready check",
|
|
228
|
+
status: verify.ready ? "complete" : "pending",
|
|
229
|
+
detail: verify.ready ? "Ready to connect from ChatGPT." : `Not ready: ${verify.blockingReasons[0] ?? "review checks"}`,
|
|
230
|
+
action: verify.ready ? "Run `computer-linker client chatgpt smoke`, then connect ChatGPT." : verify.nextActions[0],
|
|
231
|
+
},
|
|
232
|
+
];
|
|
233
|
+
const current = verify.ready
|
|
234
|
+
? null
|
|
235
|
+
: steps.find((step) => step.status === "blocked" || step.status === "current")
|
|
236
|
+
?? steps.find((step) => step.status === "pending")
|
|
237
|
+
?? null;
|
|
238
|
+
return {
|
|
239
|
+
overallStatus: verify.ready ? "ready" : steps.some((step) => step.status === "blocked") ? "blocked" : "needs_action",
|
|
240
|
+
currentStepId: current?.id ?? null,
|
|
241
|
+
effectiveMcpServerUrl: urls.effectiveMcpServerUrl,
|
|
242
|
+
detectedPublicUrl: urls.detectedPublicUrl,
|
|
243
|
+
steps,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
export function chatGptUrl(config, includeSecret = false, options = {}) {
|
|
247
|
+
const detectedPublicUrl = chatGptDetectedPublicBaseUrl(options.tunnels ?? []);
|
|
248
|
+
const publicBaseUrl = chatGptPublicBaseUrl(config, options.tunnels);
|
|
249
|
+
const publicBaseUrlSource = detectedPublicUrl && publicBaseUrl === detectedPublicUrl
|
|
250
|
+
? "running-tunnel"
|
|
251
|
+
: config.publicBaseUrl ? "configured" : null;
|
|
252
|
+
const mcpServerUrl = publicBaseUrl ? new URL("/mcp", publicBaseUrl).href : undefined;
|
|
253
|
+
const warnings = [];
|
|
254
|
+
const nextActions = [];
|
|
255
|
+
if (!mcpServerUrl) {
|
|
256
|
+
warnings.push("No public HTTPS MCP URL is configured.");
|
|
257
|
+
nextActions.push("For first setup, run `computer-linker start <workspace-path> --tunnel tailscale` to auto-save a Funnel URL; for Cloudflare/custom hostnames, pass `--url https://... --tunnel cloudflare`; for ChatGPT Tunnel mode, use `computer-linker start <workspace-path> --tunnel openai --tunnel-id tunnel_...`.");
|
|
258
|
+
}
|
|
259
|
+
else if (!mcpServerUrl.startsWith("https://")) {
|
|
260
|
+
warnings.push("ChatGPT requires an https:// MCP URL.");
|
|
261
|
+
nextActions.push("Use an HTTPS tunnel origin, then run `computer-linker config set-public-url https://...`.");
|
|
262
|
+
}
|
|
263
|
+
if (!config.ownerToken) {
|
|
264
|
+
warnings.push("ownerToken is not configured.");
|
|
265
|
+
nextActions.push("Run `computer-linker init` before exposing Computer Linker to ChatGPT.");
|
|
266
|
+
}
|
|
267
|
+
if (nextActions.length === 0) {
|
|
268
|
+
nextActions.push("Paste the MCP URL into ChatGPT custom MCP app setup and use the Authorization bearer token.");
|
|
269
|
+
}
|
|
270
|
+
return {
|
|
271
|
+
kind: "chatgpt-url",
|
|
272
|
+
schemaVersion: 1,
|
|
273
|
+
ready: Boolean(mcpServerUrl?.startsWith("https://") && config.ownerToken),
|
|
274
|
+
mcpServerUrl: mcpServerUrl ?? null,
|
|
275
|
+
publicBaseUrl: publicBaseUrl ?? null,
|
|
276
|
+
publicBaseUrlSource,
|
|
277
|
+
configuredPublicBaseUrl: config.publicBaseUrl ?? null,
|
|
278
|
+
detectedPublicUrl: detectedPublicUrl ?? null,
|
|
279
|
+
authHeader: config.ownerToken && includeSecret ? `Authorization: Bearer ${config.ownerToken}` : "Authorization: Bearer <ownerToken>",
|
|
280
|
+
warnings,
|
|
281
|
+
nextActions,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
function chatGptDetectedPublicBaseUrl(tunnels) {
|
|
285
|
+
return tunnels
|
|
286
|
+
.filter((tp) => tp.status === "running")
|
|
287
|
+
.map((tp) => tp.publicUrl)
|
|
288
|
+
.find((url) => Boolean(url));
|
|
289
|
+
}
|
|
290
|
+
export function formatChatGptUrl(report) {
|
|
291
|
+
return [
|
|
292
|
+
"Computer Linker ChatGPT URL",
|
|
293
|
+
`ready: ${report.ready ? "yes" : "no"}`,
|
|
294
|
+
`mcpServerUrl: ${report.mcpServerUrl ?? "not configured"}`,
|
|
295
|
+
`publicBaseUrl: ${report.publicBaseUrl ?? "not detected"}`,
|
|
296
|
+
`publicBaseUrlSource: ${report.publicBaseUrlSource ?? "none"}`,
|
|
297
|
+
`authHeader: ${report.authHeader}`,
|
|
298
|
+
...(report.warnings.length ? ["warnings:", ...report.warnings.map((warning) => ` - ${warning}`)] : []),
|
|
299
|
+
"next actions:",
|
|
300
|
+
...report.nextActions.map((action) => ` - ${action}`),
|
|
301
|
+
].join("\n") + "\n";
|
|
302
|
+
}
|
|
303
|
+
export async function chatGptSmoke(config, options = {}) {
|
|
304
|
+
const clientSmoke = await runWorkspaceLinkerMcpClientSmoke(config, {
|
|
305
|
+
url: options.url,
|
|
306
|
+
token: options.token,
|
|
307
|
+
includeSecret: options.includeSecret,
|
|
308
|
+
allowHttp: options.allowHttp,
|
|
309
|
+
timeoutMs: options.timeoutMs,
|
|
310
|
+
fetchImpl: options.fetchImpl,
|
|
311
|
+
clientName: "computer-linker-smoke",
|
|
312
|
+
});
|
|
313
|
+
const checks = clientSmoke.checks.map(chatGptSmokeCheck);
|
|
314
|
+
const blockingReasons = checks
|
|
315
|
+
.filter((check) => check.status === "fail")
|
|
316
|
+
.map((check) => `${check.id}: ${check.message}`);
|
|
317
|
+
const warnings = checks
|
|
318
|
+
.filter((check) => check.status === "warn")
|
|
319
|
+
.map((check) => `${check.id}: ${check.message}`);
|
|
320
|
+
return {
|
|
321
|
+
kind: "chatgpt-smoke",
|
|
322
|
+
schemaVersion: 1,
|
|
323
|
+
ready: blockingReasons.length === 0,
|
|
324
|
+
baseUrl: clientSmoke.baseUrl,
|
|
325
|
+
mcpServerUrl: clientSmoke.mcpServerUrl,
|
|
326
|
+
authHeader: clientSmoke.authHeader === "none" ? "Authorization: Bearer <ownerToken>" : clientSmoke.authHeader,
|
|
327
|
+
checks,
|
|
328
|
+
blockingReasons,
|
|
329
|
+
warnings,
|
|
330
|
+
nextActions: chatGptSmokeNextActions(blockingReasons, warnings),
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
function chatGptSmokeCheck(check) {
|
|
334
|
+
return {
|
|
335
|
+
...check,
|
|
336
|
+
id: check.id === "api-capabilities" ? "capabilities" : check.id,
|
|
337
|
+
message: chatGptSmokeText(check.message),
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
function chatGptSmokeText(value) {
|
|
341
|
+
return value
|
|
342
|
+
.replaceAll("MCP client smoke URL", "ChatGPT smoke URL")
|
|
343
|
+
.replaceAll("MCP client smoke testing", "ChatGPT smoke testing")
|
|
344
|
+
.replaceAll("cloud MCP client", "ChatGPT")
|
|
345
|
+
.replaceAll("MCP client setup", "ChatGPT custom MCP app setup");
|
|
346
|
+
}
|
|
347
|
+
export function formatChatGptSmoke(report) {
|
|
348
|
+
return [
|
|
349
|
+
"Computer Linker ChatGPT smoke",
|
|
350
|
+
`ready: ${report.ready ? "yes" : "no"}`,
|
|
351
|
+
`baseUrl: ${report.baseUrl ?? "not configured"}`,
|
|
352
|
+
`mcpServerUrl: ${report.mcpServerUrl ?? "not configured"}`,
|
|
353
|
+
`authHeader: ${report.authHeader}`,
|
|
354
|
+
"checks:",
|
|
355
|
+
...report.checks.map((check) => ` [${check.status}] ${check.id}: ${check.message}${check.statusCode ? ` (${check.statusCode})` : ""}${check.durationMs !== undefined ? ` ${check.durationMs}ms` : ""}`),
|
|
356
|
+
"next actions:",
|
|
357
|
+
...report.nextActions.map((action) => ` - ${action}`),
|
|
358
|
+
].join("\n") + "\n";
|
|
359
|
+
}
|
|
360
|
+
export function formatChatGptVerify(report) {
|
|
361
|
+
return [
|
|
362
|
+
`Computer Linker ChatGPT verify (${report.mode})`,
|
|
363
|
+
`ready: ${report.ready ? "yes" : "no"}`,
|
|
364
|
+
`mcpServerUrl: ${report.mcpServerUrl}`,
|
|
365
|
+
`auth: ${report.authMode}`,
|
|
366
|
+
"checks:",
|
|
367
|
+
...report.checks.map((check) => ` [${check.status}] ${check.id}: ${check.message}${check.detail ? ` (${check.detail})` : ""}`),
|
|
368
|
+
"next actions:",
|
|
369
|
+
...report.nextActions.map((action) => ` - ${action}`),
|
|
370
|
+
].join("\n") + "\n";
|
|
371
|
+
}
|
|
372
|
+
function chatGptSmokeNextActions(blockingReasons, warnings) {
|
|
373
|
+
const actions = new Set();
|
|
374
|
+
if (blockingReasons.some((reason) => reason.includes("base-url"))) {
|
|
375
|
+
actions.add("Set publicBaseUrl or rerun with `--url https://...`; use `--allow-http` only for local testing.");
|
|
376
|
+
}
|
|
377
|
+
if (blockingReasons.some((reason) => reason.includes("auth"))) {
|
|
378
|
+
actions.add("Run `computer-linker init` or pass `--token <ownerToken>` for the smoke test.");
|
|
379
|
+
}
|
|
380
|
+
if (blockingReasons.some((reason) => reason.includes("capabilities") || reason.includes("mcp-") || reason.includes("healthz"))) {
|
|
381
|
+
actions.add("Confirm the HTTP server is running and the tunnel routes to this machine.");
|
|
382
|
+
}
|
|
383
|
+
if (warnings.some((warning) => warning.includes("HTTP URL"))) {
|
|
384
|
+
actions.add("Use an HTTPS tunnel URL before configuring ChatGPT cloud access.");
|
|
385
|
+
}
|
|
386
|
+
if (actions.size === 0) {
|
|
387
|
+
actions.add("Use the MCP server URL and Authorization bearer token in ChatGPT custom MCP app setup.");
|
|
388
|
+
}
|
|
389
|
+
return [...actions];
|
|
390
|
+
}
|
|
391
|
+
function publicBaseUrlCheck(publicBaseUrl) {
|
|
392
|
+
if (!publicBaseUrl) {
|
|
393
|
+
return {
|
|
394
|
+
id: "public-base-url",
|
|
395
|
+
status: "fail",
|
|
396
|
+
message: "publicBaseUrl is required for ChatGPT cloud access.",
|
|
397
|
+
detail: "Run `computer-linker config set-public-url https://...` after configuring a tunnel.",
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
let parsed;
|
|
401
|
+
try {
|
|
402
|
+
parsed = new URL(publicBaseUrl);
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return {
|
|
406
|
+
id: "public-base-url",
|
|
407
|
+
status: "fail",
|
|
408
|
+
message: "publicBaseUrl must be a valid URL.",
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
if (parsed.protocol !== "https:") {
|
|
412
|
+
return {
|
|
413
|
+
id: "public-base-url",
|
|
414
|
+
status: "fail",
|
|
415
|
+
message: "publicBaseUrl must use https:// for ChatGPT.",
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
id: "public-base-url",
|
|
420
|
+
status: "pass",
|
|
421
|
+
message: "publicBaseUrl is configured with HTTPS.",
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
function mcpUrlCheck(mcpServerUrl) {
|
|
425
|
+
try {
|
|
426
|
+
const parsed = new URL(mcpServerUrl);
|
|
427
|
+
if (parsed.protocol !== "https:") {
|
|
428
|
+
return {
|
|
429
|
+
id: "mcp-url",
|
|
430
|
+
status: "fail",
|
|
431
|
+
message: "MCP server URL must use https://.",
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (!parsed.pathname.endsWith("/mcp")) {
|
|
435
|
+
return {
|
|
436
|
+
id: "mcp-url",
|
|
437
|
+
status: "warn",
|
|
438
|
+
message: "MCP server URL should end with /mcp.",
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
return {
|
|
444
|
+
id: "mcp-url",
|
|
445
|
+
status: "fail",
|
|
446
|
+
message: "MCP server URL is invalid.",
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
id: "mcp-url",
|
|
451
|
+
status: "pass",
|
|
452
|
+
message: "MCP server URL is valid for ChatGPT setup.",
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
function ownerTokenCheck(config) {
|
|
456
|
+
return config.ownerToken
|
|
457
|
+
? {
|
|
458
|
+
id: "auth",
|
|
459
|
+
status: "pass",
|
|
460
|
+
message: "ownerToken is configured; OAuth/bearer HTTP auth can be enabled.",
|
|
461
|
+
}
|
|
462
|
+
: {
|
|
463
|
+
id: "auth",
|
|
464
|
+
status: "fail",
|
|
465
|
+
message: "ownerToken is required before exposing Computer Linker to ChatGPT.",
|
|
466
|
+
detail: "Run `computer-linker init` or set COMPUTER_LINKER_OWNER_TOKEN.",
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
function workspaceCheck(config) {
|
|
470
|
+
if (config.workspaces.length === 0) {
|
|
471
|
+
return {
|
|
472
|
+
id: "workspaces",
|
|
473
|
+
status: "fail",
|
|
474
|
+
message: "At least one predefined workspace is required.",
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
id: "workspaces",
|
|
479
|
+
status: "pass",
|
|
480
|
+
message: `${config.workspaces.length} workspace(s) configured.`,
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
function toolSurfaceCheck(tools) {
|
|
484
|
+
const missing = CHATGPT_TOOLS.filter((tool) => !tools.includes(tool));
|
|
485
|
+
const extra = tools.filter((tool) => !CHATGPT_TOOLS.includes(tool));
|
|
486
|
+
if (missing.length > 0 || extra.length > 0) {
|
|
487
|
+
return {
|
|
488
|
+
id: "tool-surface",
|
|
489
|
+
status: "fail",
|
|
490
|
+
message: "ChatGPT tool surface must stay minimal and predictable.",
|
|
491
|
+
detail: `missing=${missing.join(",") || "none"} extra=${extra.join(",") || "none"}`,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
return {
|
|
495
|
+
id: "tool-surface",
|
|
496
|
+
status: "pass",
|
|
497
|
+
message: "The expected MCP tools are exposed.",
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
function modePermissionCheck(config, mode) {
|
|
501
|
+
const writeCount = config.workspaces.filter((workspace) => workspace.permissions.write).length;
|
|
502
|
+
const shellCount = config.workspaces.filter((workspace) => workspace.permissions.shell).length;
|
|
503
|
+
const codexCount = config.workspaces.filter((workspace) => workspace.permissions.codex).length;
|
|
504
|
+
if (mode === "safe" && (writeCount > 0 || shellCount > 0 || codexCount > 0)) {
|
|
505
|
+
return {
|
|
506
|
+
id: "mode-permissions",
|
|
507
|
+
status: "fail",
|
|
508
|
+
message: "Safe mode requires read/search/history/git-read style workspaces only.",
|
|
509
|
+
detail: `write=${writeCount} shell=${shellCount} codex=${codexCount}`,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
if (mode === "coding" && (shellCount > 0 || codexCount > 0)) {
|
|
513
|
+
return {
|
|
514
|
+
id: "mode-permissions",
|
|
515
|
+
status: "warn",
|
|
516
|
+
message: "Coding mode can use broad local execution, but shell/Codex should be reviewed before ChatGPT access.",
|
|
517
|
+
detail: `write=${writeCount} shell=${shellCount} codex=${codexCount}`,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
if (mode === "full" && (writeCount > 0 || shellCount > 0 || codexCount > 0)) {
|
|
521
|
+
return {
|
|
522
|
+
id: "mode-permissions",
|
|
523
|
+
status: "warn",
|
|
524
|
+
message: "Full mode exposes write and/or local execution capabilities.",
|
|
525
|
+
detail: `write=${writeCount} shell=${shellCount} codex=${codexCount}`,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
return {
|
|
529
|
+
id: "mode-permissions",
|
|
530
|
+
status: "pass",
|
|
531
|
+
message: `${mode} mode permissions are acceptable.`,
|
|
532
|
+
detail: `write=${writeCount} shell=${shellCount} codex=${codexCount}`,
|
|
533
|
+
};
|
|
534
|
+
}
|
|
535
|
+
function chatGptNextActions(blockingReasons, warnings, mode) {
|
|
536
|
+
const actions = new Set();
|
|
537
|
+
if (blockingReasons.some((reason) => reason.includes("public-base-url"))) {
|
|
538
|
+
actions.add("For first setup, run `computer-linker start <workspace-path> --tunnel tailscale` to auto-save a Funnel URL; for Cloudflare/custom hostnames, pass `--url https://... --tunnel cloudflare`; for OpenAI Secure MCP Tunnel, use `computer-linker start <workspace-path> --tunnel openai --tunnel-id tunnel_...`.");
|
|
539
|
+
}
|
|
540
|
+
if (blockingReasons.some((reason) => reason.includes("auth"))) {
|
|
541
|
+
actions.add("Run `computer-linker init` to generate an owner token before exposing the MCP server.");
|
|
542
|
+
}
|
|
543
|
+
if (blockingReasons.some((reason) => reason.includes("mode-permissions")) && mode === "safe") {
|
|
544
|
+
actions.add("Create a read-only workspace profile or rerun with `--mode coding` after reviewing write/shell/Codex permissions.");
|
|
545
|
+
}
|
|
546
|
+
if (warnings.some((warning) => warning.includes("mode-permissions"))) {
|
|
547
|
+
actions.add("Review workspace permissions and only expose shell/Codex to ChatGPT when you intend broad local execution.");
|
|
548
|
+
}
|
|
549
|
+
if (actions.size === 0) {
|
|
550
|
+
actions.add("Run `computer-linker client chatgpt profile --show-token` and use the MCP URL in ChatGPT developer mode.");
|
|
551
|
+
}
|
|
552
|
+
return [...actions];
|
|
553
|
+
}
|
|
554
|
+
function urlOrigin(value) {
|
|
555
|
+
try {
|
|
556
|
+
return new URL(value).origin;
|
|
557
|
+
}
|
|
558
|
+
catch {
|
|
559
|
+
return undefined;
|
|
560
|
+
}
|
|
561
|
+
}
|
package/dist/cli.d.ts
ADDED