@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
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { auditLogPath } from "./config.js";
|
|
2
|
+
import { readAuditEvents } from "./audit.js";
|
|
3
|
+
import { listTunnelProcesses, tunnelRuntimeEvents } from "./tunnels.js";
|
|
4
|
+
export function historyInsight(options = {}) {
|
|
5
|
+
const view = historyInsightView(options.view);
|
|
6
|
+
const limit = normalizeLimit(options.limit);
|
|
7
|
+
const events = options.events ?? readAuditEvents({
|
|
8
|
+
workspaceId: options.workspaceId,
|
|
9
|
+
query: options.query,
|
|
10
|
+
limit,
|
|
11
|
+
});
|
|
12
|
+
const mergedEvents = mergeDerivedHistoryEvents(events, {
|
|
13
|
+
...options,
|
|
14
|
+
view,
|
|
15
|
+
limit,
|
|
16
|
+
});
|
|
17
|
+
return historyInsightFromEvents(mergedEvents, {
|
|
18
|
+
view,
|
|
19
|
+
limit,
|
|
20
|
+
query: options.query,
|
|
21
|
+
workspaceId: options.workspaceId,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
export function historyInsightFromEvents(events, options = {}) {
|
|
25
|
+
const view = historyInsightView(options.view);
|
|
26
|
+
const limit = normalizeLimit(options.limit);
|
|
27
|
+
const compactEvents = events.slice(0, limit).map(compactAuditEvent);
|
|
28
|
+
const summary = {
|
|
29
|
+
totalEvents: compactEvents.length,
|
|
30
|
+
successfulEvents: compactEvents.filter((event) => event.success).length,
|
|
31
|
+
failedEvents: compactEvents.filter((event) => !event.success).length,
|
|
32
|
+
lastEvent: compactEvents[0],
|
|
33
|
+
lastWorkspaceOperation: compactEvents.find(isOperationAuditEvent),
|
|
34
|
+
recentFailures: compactEvents.filter((event) => !event.success).slice(0, 10),
|
|
35
|
+
toolCounts: counts(compactEvents.map((event) => event.tool ?? event.type)),
|
|
36
|
+
workspaceCounts: counts(compactEvents.map((event) => event.workspaceId ?? event.workspaceRef).filter((value) => Boolean(value))),
|
|
37
|
+
};
|
|
38
|
+
const failedReplay = buildFailedReplay(compactEvents);
|
|
39
|
+
const sessions = buildSessionSummaries(compactEvents);
|
|
40
|
+
const connections = buildConnectionSummaries(compactEvents);
|
|
41
|
+
const last = buildLastInsight(summary, sessions, connections, failedReplay);
|
|
42
|
+
const insight = {
|
|
43
|
+
view,
|
|
44
|
+
generatedAt: new Date().toISOString(),
|
|
45
|
+
filters: {
|
|
46
|
+
workspaceId: options.workspaceId,
|
|
47
|
+
query: options.query,
|
|
48
|
+
limit,
|
|
49
|
+
},
|
|
50
|
+
summary,
|
|
51
|
+
};
|
|
52
|
+
if (view === "last" || view === "debug_bundle") {
|
|
53
|
+
insight.last = last;
|
|
54
|
+
}
|
|
55
|
+
if (view === "timeline" || view === "debug_bundle") {
|
|
56
|
+
insight.timeline = [...compactEvents].reverse();
|
|
57
|
+
}
|
|
58
|
+
if (view === "sessions" || view === "debug_bundle") {
|
|
59
|
+
insight.sessions = sessions;
|
|
60
|
+
}
|
|
61
|
+
if (view === "connections" || view === "debug_bundle") {
|
|
62
|
+
insight.connections = connections;
|
|
63
|
+
}
|
|
64
|
+
if (view === "failed_replay" || view === "debug_bundle") {
|
|
65
|
+
insight.failedReplay = failedReplay;
|
|
66
|
+
}
|
|
67
|
+
if (view === "debug_bundle") {
|
|
68
|
+
insight.debugBundle = {
|
|
69
|
+
format: "computer-linker-debug-bundle-v1",
|
|
70
|
+
auditLogPath: auditLogPath(),
|
|
71
|
+
redactions: [
|
|
72
|
+
"Owner tokens and OAuth tokens are not written to the audit log.",
|
|
73
|
+
"File contents, patch bodies, write payloads, screenshot image bytes, and full command prompts are not included.",
|
|
74
|
+
"commandPreview is truncated to a short diagnostic preview.",
|
|
75
|
+
"Tunnel-client raw logs are converted to compact tunnel_event rows; full tunnel stderr/stdout is not exported.",
|
|
76
|
+
],
|
|
77
|
+
events: compactEvents,
|
|
78
|
+
connections,
|
|
79
|
+
failedReplay,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
return insight;
|
|
83
|
+
}
|
|
84
|
+
export function historyInsightView(value) {
|
|
85
|
+
if (value === "last" || value === "timeline" || value === "sessions" || value === "connections" || value === "failed_replay" || value === "debug_bundle")
|
|
86
|
+
return value;
|
|
87
|
+
return "summary";
|
|
88
|
+
}
|
|
89
|
+
function buildLastInsight(summary, sessions, connections, failedReplay) {
|
|
90
|
+
const failure = summary.recentFailures[0];
|
|
91
|
+
const replay = failure
|
|
92
|
+
? failedReplay.find((item) => item.timestamp === failure.timestamp)
|
|
93
|
+
: undefined;
|
|
94
|
+
return {
|
|
95
|
+
event: summary.lastEvent,
|
|
96
|
+
workspaceOperation: summary.lastWorkspaceOperation,
|
|
97
|
+
failure,
|
|
98
|
+
replay,
|
|
99
|
+
session: sessions[0],
|
|
100
|
+
connection: connections[0],
|
|
101
|
+
suggestedNextActions: lastInsightNextActions(summary, replay),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function lastInsightNextActions(summary, replay) {
|
|
105
|
+
const actions = [];
|
|
106
|
+
if (summary.recentFailures.some((event) => event.type === "tunnel_event")) {
|
|
107
|
+
actions.push("Call history_insight with view=connections to inspect tunnel sessions and request IDs.");
|
|
108
|
+
}
|
|
109
|
+
if (summary.failedEvents > 0) {
|
|
110
|
+
actions.push("Call history_insight with view=failed_replay to inspect replay templates for recent failures.");
|
|
111
|
+
}
|
|
112
|
+
if (replay?.replayable) {
|
|
113
|
+
actions.push("Replay the failed operation after confirming the workspace and target are still correct.");
|
|
114
|
+
}
|
|
115
|
+
else if (replay?.requiresInput?.length) {
|
|
116
|
+
actions.push(`Ask for or reconstruct missing replay input: ${replay.requiresInput.join(", ")}.`);
|
|
117
|
+
}
|
|
118
|
+
if (summary.lastWorkspaceOperation) {
|
|
119
|
+
actions.push("Call history_insight with view=timeline and a small limit if more context is needed.");
|
|
120
|
+
}
|
|
121
|
+
if (actions.length === 0) {
|
|
122
|
+
actions.push("No recent failure was found; continue with the next requested workspace operation.");
|
|
123
|
+
}
|
|
124
|
+
return actions;
|
|
125
|
+
}
|
|
126
|
+
function compactAuditEvent(event) {
|
|
127
|
+
return {
|
|
128
|
+
timestamp: event.timestamp,
|
|
129
|
+
type: event.type,
|
|
130
|
+
success: event.success,
|
|
131
|
+
tool: event.tool,
|
|
132
|
+
workspaceId: event.workspaceId,
|
|
133
|
+
workspaceRef: event.workspaceRef,
|
|
134
|
+
requestPath: event.requestPath,
|
|
135
|
+
remoteAddress: event.remoteAddress,
|
|
136
|
+
path: event.path,
|
|
137
|
+
workingDirectory: event.workingDirectory,
|
|
138
|
+
commandPreview: event.commandPreview,
|
|
139
|
+
operation: event.operation,
|
|
140
|
+
target: event.target,
|
|
141
|
+
detail: event.detail,
|
|
142
|
+
replay: event.replay,
|
|
143
|
+
error: event.error,
|
|
144
|
+
durationMs: event.durationMs,
|
|
145
|
+
provider: event.provider,
|
|
146
|
+
tunnelId: event.tunnelId,
|
|
147
|
+
externalSessionId: event.externalSessionId,
|
|
148
|
+
requestId: event.requestId,
|
|
149
|
+
cmdRequestId: event.cmdRequestId,
|
|
150
|
+
rpcRequestId: event.rpcRequestId,
|
|
151
|
+
tunnelRequestId: event.tunnelRequestId,
|
|
152
|
+
severity: event.severity,
|
|
153
|
+
statusCode: event.statusCode,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
function mergeDerivedHistoryEvents(events, options) {
|
|
157
|
+
const view = historyInsightView(options.view);
|
|
158
|
+
const limit = normalizeLimit(options.limit);
|
|
159
|
+
const includeInfo = view === "connections" || view === "debug_bundle";
|
|
160
|
+
const derivedEvents = tunnelRuntimeEvents(listTunnelProcesses(), {
|
|
161
|
+
includeInfo,
|
|
162
|
+
limit: includeInfo ? Math.max(limit, 200) : Math.min(limit, 100),
|
|
163
|
+
})
|
|
164
|
+
.map(tunnelRuntimeEventToAuditEvent)
|
|
165
|
+
.filter((event) => historyEventMatchesFilters(event, options));
|
|
166
|
+
return [...events, ...derivedEvents]
|
|
167
|
+
.filter((event) => historyEventMatchesFilters(event, options))
|
|
168
|
+
.sort((a, b) => b.timestamp.localeCompare(a.timestamp))
|
|
169
|
+
.slice(0, limit);
|
|
170
|
+
}
|
|
171
|
+
function tunnelRuntimeEventToAuditEvent(event) {
|
|
172
|
+
const success = event.severity === "info";
|
|
173
|
+
const detail = tunnelAuditDetail(event);
|
|
174
|
+
return {
|
|
175
|
+
timestamp: event.timestamp,
|
|
176
|
+
type: "tunnel_event",
|
|
177
|
+
success,
|
|
178
|
+
tool: `tunnel:${event.provider}`,
|
|
179
|
+
requestPath: event.rpcMethod,
|
|
180
|
+
remoteAddress: event.tunnelRequestId,
|
|
181
|
+
operation: event.kind,
|
|
182
|
+
detail,
|
|
183
|
+
error: success ? undefined : event.detail ?? event.message,
|
|
184
|
+
provider: event.provider,
|
|
185
|
+
tunnelId: event.tunnelId,
|
|
186
|
+
externalSessionId: event.sessionId,
|
|
187
|
+
requestId: event.requestId,
|
|
188
|
+
cmdRequestId: event.cmdRequestId,
|
|
189
|
+
rpcRequestId: event.rpcRequestId,
|
|
190
|
+
tunnelRequestId: event.tunnelRequestId,
|
|
191
|
+
severity: event.severity,
|
|
192
|
+
statusCode: event.statusCode,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function tunnelAuditDetail(event) {
|
|
196
|
+
return [
|
|
197
|
+
event.message,
|
|
198
|
+
event.rpcMethod,
|
|
199
|
+
event.statusCode === undefined ? undefined : `status=${event.statusCode}`,
|
|
200
|
+
event.detail,
|
|
201
|
+
].filter(Boolean).join(" · ");
|
|
202
|
+
}
|
|
203
|
+
function historyEventMatchesFilters(event, options) {
|
|
204
|
+
if (options.workspaceId && event.workspaceId !== options.workspaceId && event.workspaceRef !== options.workspaceId) {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
if (options.query && !historySearchText(event).includes(options.query.toLowerCase())) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
function historySearchText(event) {
|
|
213
|
+
return [
|
|
214
|
+
event.timestamp,
|
|
215
|
+
event.type,
|
|
216
|
+
event.tool,
|
|
217
|
+
event.workspaceId,
|
|
218
|
+
event.workspaceRoot,
|
|
219
|
+
event.workspaceRef,
|
|
220
|
+
event.path,
|
|
221
|
+
event.requestPath,
|
|
222
|
+
event.remoteAddress,
|
|
223
|
+
event.workingDirectory,
|
|
224
|
+
event.commandPreview,
|
|
225
|
+
event.operation,
|
|
226
|
+
event.target,
|
|
227
|
+
event.detail,
|
|
228
|
+
event.error,
|
|
229
|
+
event.provider,
|
|
230
|
+
event.tunnelId,
|
|
231
|
+
event.externalSessionId,
|
|
232
|
+
event.requestId,
|
|
233
|
+
event.cmdRequestId,
|
|
234
|
+
event.rpcRequestId,
|
|
235
|
+
event.tunnelRequestId,
|
|
236
|
+
event.severity,
|
|
237
|
+
event.statusCode === undefined ? undefined : String(event.statusCode),
|
|
238
|
+
]
|
|
239
|
+
.filter(Boolean)
|
|
240
|
+
.join("\n")
|
|
241
|
+
.toLowerCase();
|
|
242
|
+
}
|
|
243
|
+
function isOperationAuditEvent(event) {
|
|
244
|
+
return event.tool === "computer_operation" ||
|
|
245
|
+
event.tool === "workspace_operation" ||
|
|
246
|
+
event.tool === "workspace_operation.batch_item";
|
|
247
|
+
}
|
|
248
|
+
function buildSessionSummaries(events) {
|
|
249
|
+
const groups = new Map();
|
|
250
|
+
for (const event of events) {
|
|
251
|
+
const key = sessionKey(event);
|
|
252
|
+
groups.set(key, [...(groups.get(key) ?? []), event]);
|
|
253
|
+
}
|
|
254
|
+
return [...groups.entries()]
|
|
255
|
+
.map(([key, sessionEvents]) => {
|
|
256
|
+
const newest = sessionEvents[0];
|
|
257
|
+
const oldest = sessionEvents[sessionEvents.length - 1];
|
|
258
|
+
const workspaceRef = newest.workspaceRef ?? newest.workspaceId;
|
|
259
|
+
const workspaceId = newest.workspaceId;
|
|
260
|
+
const surface = sessionSurface(newest);
|
|
261
|
+
const scope = key.startsWith("workspace:") ? "workspace" : "surface";
|
|
262
|
+
return {
|
|
263
|
+
key,
|
|
264
|
+
scope,
|
|
265
|
+
workspaceId,
|
|
266
|
+
workspaceRef,
|
|
267
|
+
surface,
|
|
268
|
+
startedAt: oldest.timestamp,
|
|
269
|
+
lastActivityAt: newest.timestamp,
|
|
270
|
+
totalEvents: sessionEvents.length,
|
|
271
|
+
successfulEvents: sessionEvents.filter((event) => event.success).length,
|
|
272
|
+
failedEvents: sessionEvents.filter((event) => !event.success).length,
|
|
273
|
+
tools: counts(sessionEvents.map((event) => event.tool ?? event.type)),
|
|
274
|
+
operations: counts(sessionEvents.map((event) => event.operation).filter((value) => Boolean(value))),
|
|
275
|
+
lastEvent: newest,
|
|
276
|
+
recentFailures: sessionEvents.filter((event) => !event.success).slice(0, 5),
|
|
277
|
+
};
|
|
278
|
+
})
|
|
279
|
+
.sort((left, right) => right.lastActivityAt.localeCompare(left.lastActivityAt));
|
|
280
|
+
}
|
|
281
|
+
function buildConnectionSummaries(events) {
|
|
282
|
+
const groups = new Map();
|
|
283
|
+
for (const event of events) {
|
|
284
|
+
const key = connectionKey(event);
|
|
285
|
+
if (!key)
|
|
286
|
+
continue;
|
|
287
|
+
groups.set(key, [...(groups.get(key) ?? []), event]);
|
|
288
|
+
}
|
|
289
|
+
return [...groups.entries()]
|
|
290
|
+
.map(([key, connectionEvents]) => {
|
|
291
|
+
const newest = connectionEvents[0];
|
|
292
|
+
const oldest = connectionEvents[connectionEvents.length - 1];
|
|
293
|
+
const requestIds = new Set(connectionEvents.flatMap((event) => ([event.requestId, event.cmdRequestId, event.rpcRequestId, event.tunnelRequestId].filter((value) => Boolean(value)))));
|
|
294
|
+
return {
|
|
295
|
+
key,
|
|
296
|
+
scope: connectionScope(key),
|
|
297
|
+
provider: newest.provider,
|
|
298
|
+
tunnelId: newest.tunnelId,
|
|
299
|
+
externalSessionId: newest.externalSessionId,
|
|
300
|
+
remoteAddress: newest.remoteAddress,
|
|
301
|
+
startedAt: oldest.timestamp,
|
|
302
|
+
lastActivityAt: newest.timestamp,
|
|
303
|
+
totalEvents: connectionEvents.length,
|
|
304
|
+
successfulEvents: connectionEvents.filter((event) => event.success).length,
|
|
305
|
+
failedEvents: connectionEvents.filter((event) => !event.success).length,
|
|
306
|
+
requestCount: requestIds.size,
|
|
307
|
+
tools: counts(connectionEvents.map((event) => event.tool ?? event.type)),
|
|
308
|
+
operations: counts(connectionEvents.map((event) => event.operation).filter((value) => Boolean(value))),
|
|
309
|
+
lastEvent: newest,
|
|
310
|
+
recentFailures: connectionEvents.filter((event) => !event.success).slice(0, 5),
|
|
311
|
+
};
|
|
312
|
+
})
|
|
313
|
+
.sort((left, right) => right.lastActivityAt.localeCompare(left.lastActivityAt));
|
|
314
|
+
}
|
|
315
|
+
function connectionKey(event) {
|
|
316
|
+
if (event.externalSessionId)
|
|
317
|
+
return `tunnel:${event.provider ?? "unknown"}:${event.externalSessionId}`;
|
|
318
|
+
if (event.tunnelId)
|
|
319
|
+
return `tunnel:${event.provider ?? "unknown"}:${event.tunnelId}`;
|
|
320
|
+
const mcpSessionId = mcpSessionIdFromEvent(event);
|
|
321
|
+
if (mcpSessionId)
|
|
322
|
+
return `mcp:${mcpSessionId}`;
|
|
323
|
+
const workspace = event.workspaceId ?? event.workspaceRef;
|
|
324
|
+
if (workspace)
|
|
325
|
+
return `workspace:${workspace}`;
|
|
326
|
+
if (event.remoteAddress)
|
|
327
|
+
return `surface:${event.remoteAddress}`;
|
|
328
|
+
return event.tool || event.requestPath ? `surface:${sessionSurface(event)}` : undefined;
|
|
329
|
+
}
|
|
330
|
+
function connectionScope(key) {
|
|
331
|
+
if (key.startsWith("tunnel:"))
|
|
332
|
+
return "tunnel";
|
|
333
|
+
if (key.startsWith("mcp:"))
|
|
334
|
+
return "mcp";
|
|
335
|
+
if (key.startsWith("workspace:"))
|
|
336
|
+
return "workspace";
|
|
337
|
+
return "surface";
|
|
338
|
+
}
|
|
339
|
+
function mcpSessionIdFromEvent(event) {
|
|
340
|
+
if (event.type !== "mcp_session" || !event.detail)
|
|
341
|
+
return undefined;
|
|
342
|
+
const match = /(?:created|session):\s*([A-Za-z0-9_.:-]+)/.exec(event.detail);
|
|
343
|
+
return match?.[1];
|
|
344
|
+
}
|
|
345
|
+
function sessionKey(event) {
|
|
346
|
+
const workspace = event.workspaceId ?? event.workspaceRef;
|
|
347
|
+
if (workspace)
|
|
348
|
+
return `workspace:${workspace}`;
|
|
349
|
+
return `surface:${sessionSurface(event)}`;
|
|
350
|
+
}
|
|
351
|
+
function sessionSurface(event) {
|
|
352
|
+
return event.tool ?? event.requestPath ?? event.type;
|
|
353
|
+
}
|
|
354
|
+
function buildFailedReplay(events) {
|
|
355
|
+
return events
|
|
356
|
+
.filter((event) => !event.success)
|
|
357
|
+
.slice(0, 20)
|
|
358
|
+
.map((event) => {
|
|
359
|
+
const workspace = event.workspaceId ?? event.workspaceRef;
|
|
360
|
+
const op = event.operation ?? inferOperation(event);
|
|
361
|
+
if (!workspace || !op || (event.tool !== "workspace_operation" && event.tool !== "workspace_operation.batch_item")) {
|
|
362
|
+
return {
|
|
363
|
+
timestamp: event.timestamp,
|
|
364
|
+
error: event.error,
|
|
365
|
+
replayable: false,
|
|
366
|
+
reason: "Audit event does not contain enough workspace operation metadata to build a replay request.",
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
if (event.replay) {
|
|
370
|
+
return replayItemFromTemplate(event, workspace, event.replay);
|
|
371
|
+
}
|
|
372
|
+
const target = event.target ?? event.path ?? event.workingDirectory;
|
|
373
|
+
const requiresInput = sensitiveReplayInputs(op);
|
|
374
|
+
return {
|
|
375
|
+
timestamp: event.timestamp,
|
|
376
|
+
error: event.error,
|
|
377
|
+
replayable: requiresInput.length === 0,
|
|
378
|
+
reason: requiresInput.length > 0
|
|
379
|
+
? `Full ${requiresInput.join("/")} text is not stored in old audit events; provide it before replaying.`
|
|
380
|
+
: event.commandPreview ? "Replay uses stored operation metadata; command/prompt text is only available as a truncated preview." : undefined,
|
|
381
|
+
requiresInput: requiresInput.length > 0 ? requiresInput : undefined,
|
|
382
|
+
request: {
|
|
383
|
+
action: "workspace_operation",
|
|
384
|
+
workspace,
|
|
385
|
+
input: {
|
|
386
|
+
op,
|
|
387
|
+
target,
|
|
388
|
+
input: replayInput(event),
|
|
389
|
+
options: {},
|
|
390
|
+
},
|
|
391
|
+
},
|
|
392
|
+
};
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
function replayItemFromTemplate(event, workspace, replay) {
|
|
396
|
+
return {
|
|
397
|
+
timestamp: event.timestamp,
|
|
398
|
+
error: event.error,
|
|
399
|
+
replayable: replay.replayable,
|
|
400
|
+
reason: replay.reason,
|
|
401
|
+
requiresInput: replay.requiresInput,
|
|
402
|
+
request: {
|
|
403
|
+
action: replay.action,
|
|
404
|
+
workspace,
|
|
405
|
+
input: replay.input,
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function replayInput(event) {
|
|
410
|
+
if (event.operation === "package_run" || event.operation === "package_start") {
|
|
411
|
+
return event.detail ? { script: event.detail } : {};
|
|
412
|
+
}
|
|
413
|
+
if (event.operation === "search_text") {
|
|
414
|
+
return event.detail ? { query: event.detail } : {};
|
|
415
|
+
}
|
|
416
|
+
if (event.operation === "find_files") {
|
|
417
|
+
return event.detail ? { pattern: event.detail } : {};
|
|
418
|
+
}
|
|
419
|
+
if (event.operation === "process_read" || event.operation === "process_stop") {
|
|
420
|
+
return event.detail ? { processId: event.detail } : {};
|
|
421
|
+
}
|
|
422
|
+
return {};
|
|
423
|
+
}
|
|
424
|
+
function sensitiveReplayInputs(op) {
|
|
425
|
+
if (op === "command" || op === "process_start")
|
|
426
|
+
return ["command"];
|
|
427
|
+
if (op === "codex" ||
|
|
428
|
+
op === "codex_start" ||
|
|
429
|
+
op === "codex_plan" ||
|
|
430
|
+
op === "codex_review" ||
|
|
431
|
+
op === "codex_fix" ||
|
|
432
|
+
op === "codex_test" ||
|
|
433
|
+
op === "codex_continue") {
|
|
434
|
+
return ["prompt"];
|
|
435
|
+
}
|
|
436
|
+
return [];
|
|
437
|
+
}
|
|
438
|
+
function inferOperation(event) {
|
|
439
|
+
if (!event.detail)
|
|
440
|
+
return undefined;
|
|
441
|
+
const batchMatch = /^batch\[\d+\]:\s+([a-z_]+)$/.exec(event.detail);
|
|
442
|
+
if (batchMatch)
|
|
443
|
+
return batchMatch[1];
|
|
444
|
+
if (/^[a-z_]+$/.test(event.detail))
|
|
445
|
+
return event.detail;
|
|
446
|
+
return undefined;
|
|
447
|
+
}
|
|
448
|
+
function counts(values) {
|
|
449
|
+
const result = {};
|
|
450
|
+
for (const value of values) {
|
|
451
|
+
result[value] = (result[value] ?? 0) + 1;
|
|
452
|
+
}
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
function normalizeLimit(value) {
|
|
456
|
+
return Number.isInteger(value) && value && value > 0 ? Math.min(value, 1000) : 200;
|
|
457
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function isAuthorizedLocalPortRequest(req, ownerToken) {
|
|
2
|
+
if (!ownerToken)
|
|
3
|
+
return isLoopbackRequest(req);
|
|
4
|
+
const authorization = req.header("authorization") ?? "";
|
|
5
|
+
const bearerPrefix = "Bearer ";
|
|
6
|
+
if (authorization.startsWith(bearerPrefix) && authorization.slice(bearerPrefix.length) === ownerToken) {
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
return req.header("x-computer-linker-token") === ownerToken ||
|
|
10
|
+
req.header("x-workspace-linker-token") === ownerToken ||
|
|
11
|
+
req.header("x-localport-token") === ownerToken;
|
|
12
|
+
}
|
|
13
|
+
export function isLoopbackRequest(req) {
|
|
14
|
+
return req.ip === "127.0.0.1" || req.ip === "::1" || req.ip === "::ffff:127.0.0.1";
|
|
15
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type McpToolSurface = "generic" | "compatibility";
|
|
2
|
+
export declare const genericMcpTools: readonly ["get_computer_info", "computer_operation", "get_operation_history"];
|
|
3
|
+
export declare const compatibilityMcpTools: readonly ["get_capabilities", "list_workspaces", "open_workspace", "read", "ls", "grep", "glob", "create_file", "workspace_operation"];
|
|
4
|
+
export declare function mcpToolSurface(): McpToolSurface;
|
|
5
|
+
export declare function exposedMcpTools(surface?: McpToolSurface): string[];
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const genericMcpTools = ["get_computer_info", "computer_operation", "get_operation_history"];
|
|
2
|
+
export const compatibilityMcpTools = [
|
|
3
|
+
"get_capabilities",
|
|
4
|
+
"list_workspaces",
|
|
5
|
+
"open_workspace",
|
|
6
|
+
"read",
|
|
7
|
+
"ls",
|
|
8
|
+
"grep",
|
|
9
|
+
"glob",
|
|
10
|
+
"create_file",
|
|
11
|
+
"workspace_operation",
|
|
12
|
+
];
|
|
13
|
+
export function mcpToolSurface() {
|
|
14
|
+
const raw = (process.env.COMPUTER_LINKER_MCP_TOOL_SURFACE ?? process.env.WORKSPACE_LINKER_MCP_TOOL_SURFACE ?? process.env.LOCALPORT_MCP_TOOL_SURFACE ?? "generic")
|
|
15
|
+
.trim()
|
|
16
|
+
.toLowerCase();
|
|
17
|
+
if (raw === "compatibility" || raw === "legacy" || raw === "all")
|
|
18
|
+
return "compatibility";
|
|
19
|
+
return "generic";
|
|
20
|
+
}
|
|
21
|
+
export function exposedMcpTools(surface = mcpToolSurface()) {
|
|
22
|
+
return surface === "compatibility"
|
|
23
|
+
? [...genericMcpTools, ...compatibilityMcpTools]
|
|
24
|
+
: [...genericMcpTools];
|
|
25
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { Response } from "express";
|
|
2
|
+
import type { OAuthRegisteredClientsStore } from "@modelcontextprotocol/sdk/server/auth/clients.js";
|
|
3
|
+
import type { AuthorizationParams, OAuthServerProvider } from "@modelcontextprotocol/sdk/server/auth/provider.js";
|
|
4
|
+
import type { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
|
|
5
|
+
import type { OAuthClientInformationFull, OAuthTokenRevocationRequest, OAuthTokens } from "@modelcontextprotocol/sdk/shared/auth.js";
|
|
6
|
+
export interface LocalPortOAuthConfig {
|
|
7
|
+
ownerToken: string;
|
|
8
|
+
scopes: string[];
|
|
9
|
+
accessTokenTtlSeconds: number;
|
|
10
|
+
refreshTokenTtlSeconds: number;
|
|
11
|
+
}
|
|
12
|
+
interface TokenRecord {
|
|
13
|
+
clientId: string;
|
|
14
|
+
scopes: string[];
|
|
15
|
+
expiresAt: number;
|
|
16
|
+
resource?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class OAuthStateStore implements OAuthRegisteredClientsStore {
|
|
19
|
+
private readonly statePath?;
|
|
20
|
+
private readonly clients;
|
|
21
|
+
private readonly accessTokens;
|
|
22
|
+
private readonly refreshTokens;
|
|
23
|
+
constructor(statePath?: string | undefined);
|
|
24
|
+
getClient(clientId: string): OAuthClientInformationFull | undefined;
|
|
25
|
+
registerClient(client: Omit<OAuthClientInformationFull, "client_id" | "client_id_issued_at">): OAuthClientInformationFull;
|
|
26
|
+
getAccessToken(token: string): TokenRecord | undefined;
|
|
27
|
+
setAccessToken(token: string, record: TokenRecord): void;
|
|
28
|
+
deleteAccessToken(token: string): void;
|
|
29
|
+
getRefreshToken(token: string): TokenRecord | undefined;
|
|
30
|
+
setRefreshToken(token: string, record: TokenRecord): void;
|
|
31
|
+
deleteRefreshToken(token: string): void;
|
|
32
|
+
private pruneExpiredTokens;
|
|
33
|
+
private save;
|
|
34
|
+
}
|
|
35
|
+
export declare class LocalPortOAuthProvider implements OAuthServerProvider {
|
|
36
|
+
private readonly config;
|
|
37
|
+
readonly clientsStore: OAuthStateStore;
|
|
38
|
+
private readonly codes;
|
|
39
|
+
private readonly resourceServerUrl;
|
|
40
|
+
constructor(config: LocalPortOAuthConfig, mcpServerUrl: URL, options?: {
|
|
41
|
+
statePath?: string;
|
|
42
|
+
});
|
|
43
|
+
authorize(client: OAuthClientInformationFull, params: AuthorizationParams, res: Response): Promise<void>;
|
|
44
|
+
challengeForAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string): Promise<string>;
|
|
45
|
+
exchangeAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string, _codeVerifier?: string, redirectUri?: string, resource?: URL): Promise<OAuthTokens>;
|
|
46
|
+
exchangeRefreshToken(client: OAuthClientInformationFull, refreshToken: string, scopes?: string[], resource?: URL): Promise<OAuthTokens>;
|
|
47
|
+
verifyAccessToken(token: string): Promise<AuthInfo>;
|
|
48
|
+
revokeToken(_client: OAuthClientInformationFull, request: OAuthTokenRevocationRequest): Promise<void>;
|
|
49
|
+
private validCodeRecord;
|
|
50
|
+
private issueTokens;
|
|
51
|
+
}
|
|
52
|
+
export {};
|