@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/api.js
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import { errorMessage, readAuditEvents, writeAuditEvent, writeAuthFailureEvent } from "./audit.js";
|
|
2
|
+
import { workspaceCapabilityPolicy } from "./capability-policy.js";
|
|
3
|
+
import { getLocalPortCapabilities, getLocalPortDoctor } from "./capabilities.js";
|
|
4
|
+
import { chatGptSetupStatus } from "./chatgpt.js";
|
|
5
|
+
import { computerOperationContract, publicComputerOperationRegistry } from "./computer-operation-registry.js";
|
|
6
|
+
import { computerOperationAuditFields, getComputerInfo, getMcpClientSetup, getOperationHistory, runComputerOperation } from "./computer-contract.js";
|
|
7
|
+
import { loadConfig } from "./config.js";
|
|
8
|
+
import { historyInsight } from "./history-insights.js";
|
|
9
|
+
import { isAuthorizedLocalPortRequest } from "./http-auth.js";
|
|
10
|
+
import { PermissionDeniedError } from "./permissions.js";
|
|
11
|
+
import { parseChatGptProfileMode } from "./profile.js";
|
|
12
|
+
import { listTunnelProcesses } from "./tunnels.js";
|
|
13
|
+
import { WorkspaceRegistry } from "./workspaces.js";
|
|
14
|
+
import { allowedWorkspaceOperations, normalizeWorkspaceOperationInput, publicWorkspaceOperationRegistry, runWorkspaceOperation, workspaceOperationContract, workspaceOperationRegistry, workspaceOperationAuditFields, } from "./workspace-operations.js";
|
|
15
|
+
export function registerApiRoutes(app) {
|
|
16
|
+
app.use("/api/v1", (req, res, next) => {
|
|
17
|
+
if (!isAuthorizedLocalPortRequest(req, loadConfig().ownerToken)) {
|
|
18
|
+
writeAuthFailureEvent({
|
|
19
|
+
surface: "api",
|
|
20
|
+
method: req.method,
|
|
21
|
+
requestPath: requestPath(req),
|
|
22
|
+
remoteAddress: req.ip,
|
|
23
|
+
});
|
|
24
|
+
res.status(401).json({ ok: false, error: "Unauthorized" });
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
next();
|
|
28
|
+
});
|
|
29
|
+
app.get("/api/v1/health", (_req, res) => {
|
|
30
|
+
const config = loadConfig();
|
|
31
|
+
res.json({ ok: true, data: { name: "computer-linker", machineId: config.machineId, machineName: config.machineName } });
|
|
32
|
+
});
|
|
33
|
+
app.get("/api/v1/capabilities", (_req, res) => {
|
|
34
|
+
res.json({ ok: true, data: getLocalPortCapabilities() });
|
|
35
|
+
});
|
|
36
|
+
app.get("/api/v1/workspaces", (_req, res) => {
|
|
37
|
+
res.json({ ok: true, data: workspacesData() });
|
|
38
|
+
});
|
|
39
|
+
app.get("/api/v1/history", (req, res) => {
|
|
40
|
+
res.json({ ok: true, data: historyData(req.query) });
|
|
41
|
+
});
|
|
42
|
+
app.post("/api/v1/workspace-operation", apiRoute(async (req) => {
|
|
43
|
+
const input = workspaceOperationInput(req.body);
|
|
44
|
+
return withWorkspace(req.body, "workspace_operation", workspaceOperationAuditFields(input), async (registry, workspace) => (runWorkspaceOperation(registry, workspace, input)));
|
|
45
|
+
}));
|
|
46
|
+
app.post("/api/v1/control", apiRoute(async (req) => control(req)));
|
|
47
|
+
}
|
|
48
|
+
function apiRoute(handler) {
|
|
49
|
+
return (req, res) => {
|
|
50
|
+
handler(req)
|
|
51
|
+
.then((data) => res.json({ ok: true, data }))
|
|
52
|
+
.catch((error) => sendApiError(res, error));
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
async function withWorkspace(body, tool, fields, run) {
|
|
56
|
+
const workspaceRef = requiredString(body.workspace ?? body.workspaceRef, "workspace");
|
|
57
|
+
const registry = new WorkspaceRegistry(loadConfig());
|
|
58
|
+
const workspace = await registry.openWorkspace(workspaceRef);
|
|
59
|
+
return auditedApiCall(tool, {
|
|
60
|
+
workspaceId: workspace.exposedPath.id,
|
|
61
|
+
workspaceRoot: workspace.root,
|
|
62
|
+
workspaceRef,
|
|
63
|
+
...fields,
|
|
64
|
+
}, async () => run(registry, workspace));
|
|
65
|
+
}
|
|
66
|
+
async function control(req) {
|
|
67
|
+
const action = requiredString(req.body.action, "action");
|
|
68
|
+
switch (action) {
|
|
69
|
+
case "get_computer_info":
|
|
70
|
+
case "computer_info":
|
|
71
|
+
return getComputerInfo();
|
|
72
|
+
case "client_setup":
|
|
73
|
+
case "mcp_client_setup":
|
|
74
|
+
return getMcpClientSetup({ tunnels: listTunnelProcesses() });
|
|
75
|
+
case "get_capabilities":
|
|
76
|
+
case "capabilities":
|
|
77
|
+
return getLocalPortCapabilities();
|
|
78
|
+
case "doctor":
|
|
79
|
+
return getLocalPortDoctor();
|
|
80
|
+
case "chatgpt_setup":
|
|
81
|
+
return chatGptSetupData(req.body.input && typeof req.body.input === "object" ? req.body.input : req.body);
|
|
82
|
+
case "list_workspaces":
|
|
83
|
+
case "workspaces":
|
|
84
|
+
return workspacesData();
|
|
85
|
+
case "history":
|
|
86
|
+
return historyData(req.body.filters && typeof req.body.filters === "object" ? req.body.filters : req.body);
|
|
87
|
+
case "history_insight":
|
|
88
|
+
return historyInsightData(req.body.filters && typeof req.body.filters === "object" ? req.body.filters : req.body);
|
|
89
|
+
case "get_operation_history":
|
|
90
|
+
case "operation_history":
|
|
91
|
+
return getOperationHistory(req.body.input && typeof req.body.input === "object" ? req.body.input : req.body);
|
|
92
|
+
case "operation_registry":
|
|
93
|
+
return operationRegistryData(req.body.input && typeof req.body.input === "object" ? req.body.input : req.body);
|
|
94
|
+
case "computer_operation_registry":
|
|
95
|
+
return computerOperationRegistryData(req.body.input && typeof req.body.input === "object" ? req.body.input : req.body);
|
|
96
|
+
case "workspace_operation_registry":
|
|
97
|
+
return workspaceOperationRegistryData(req.body.input && typeof req.body.input === "object" ? req.body.input : req.body);
|
|
98
|
+
case "computer_operation":
|
|
99
|
+
return auditedApiCall("computer_operation", await computerOperationAuditFields({
|
|
100
|
+
scope: optionalString(req.body.scope),
|
|
101
|
+
op: optionalString(req.body.op),
|
|
102
|
+
target: optionalString(req.body.target),
|
|
103
|
+
input: req.body.input && typeof req.body.input === "object" ? req.body.input : {},
|
|
104
|
+
options: req.body.options && typeof req.body.options === "object" ? req.body.options : {},
|
|
105
|
+
}), async () => runComputerOperation({
|
|
106
|
+
scope: optionalString(req.body.scope),
|
|
107
|
+
op: optionalString(req.body.op),
|
|
108
|
+
target: optionalString(req.body.target),
|
|
109
|
+
input: req.body.input && typeof req.body.input === "object" ? req.body.input : {},
|
|
110
|
+
options: req.body.options && typeof req.body.options === "object" ? req.body.options : {},
|
|
111
|
+
}), operationResultSucceeded);
|
|
112
|
+
case "workspace_operation":
|
|
113
|
+
case "operation": {
|
|
114
|
+
const body = controlWorkspaceOperationBody(req.body);
|
|
115
|
+
const input = workspaceOperationInput(body);
|
|
116
|
+
return withWorkspace(body, "workspace_operation", workspaceOperationAuditFields(input), async (registry, workspace) => (runWorkspaceOperation(registry, workspace, input)));
|
|
117
|
+
}
|
|
118
|
+
default:
|
|
119
|
+
throw new Error("action must be one of: get_computer_info, client_setup, get_capabilities, doctor, list_workspaces, history, history_insight, get_operation_history, operation_registry, computer_operation_registry, workspace_operation_registry, computer_operation, workspace_operation, operation");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function controlWorkspaceOperationBody(body) {
|
|
123
|
+
if (body.op || body.operation)
|
|
124
|
+
return body;
|
|
125
|
+
const inputBody = body.input && typeof body.input === "object" ? body.input : undefined;
|
|
126
|
+
if (!inputBody)
|
|
127
|
+
return body;
|
|
128
|
+
return { ...inputBody, workspace: body.workspace ?? inputBody.workspace };
|
|
129
|
+
}
|
|
130
|
+
function workspacesData() {
|
|
131
|
+
const config = loadConfig();
|
|
132
|
+
const registry = new WorkspaceRegistry(config);
|
|
133
|
+
return {
|
|
134
|
+
machineId: config.machineId,
|
|
135
|
+
machineName: config.machineName,
|
|
136
|
+
workspaces: registry.listDefinedWorkspaces().map((workspace) => ({
|
|
137
|
+
...workspace,
|
|
138
|
+
capabilityPolicy: workspaceCapabilityPolicy(workspace.permissions),
|
|
139
|
+
allowedOperations: allowedWorkspaceOperations(workspace.permissions),
|
|
140
|
+
})),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function chatGptSetupData(input) {
|
|
144
|
+
return chatGptSetupStatus(loadConfig(), parseChatGptProfileMode(optionalString(input.mode), "chatgpt_setup mode"), {
|
|
145
|
+
tunnels: listTunnelProcesses(),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
function historyData(input) {
|
|
149
|
+
return {
|
|
150
|
+
events: readAuditEvents({
|
|
151
|
+
type: auditType(input.type),
|
|
152
|
+
success: optionalBoolean(input.success),
|
|
153
|
+
tool: optionalString(input.tool),
|
|
154
|
+
workspaceId: optionalString(input.workspaceId),
|
|
155
|
+
query: optionalString(input.q ?? input.query),
|
|
156
|
+
limit: optionalPositiveInteger(input.limit),
|
|
157
|
+
}),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
function historyInsightData(input) {
|
|
161
|
+
return historyInsight({
|
|
162
|
+
view: optionalString(input.view),
|
|
163
|
+
workspaceId: optionalString(input.workspaceId ?? input.workspace),
|
|
164
|
+
query: optionalString(input.q ?? input.query),
|
|
165
|
+
limit: optionalPositiveInteger(input.limit),
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function operationRegistryData(input) {
|
|
169
|
+
const contract = optionalString(input.contract ?? input.compatibility);
|
|
170
|
+
return contract === "workspace"
|
|
171
|
+
? workspaceOperationRegistryData(input)
|
|
172
|
+
: computerOperationRegistryData(input);
|
|
173
|
+
}
|
|
174
|
+
function computerOperationRegistryData(input) {
|
|
175
|
+
const category = optionalString(input.category);
|
|
176
|
+
const permission = optionalString(input.permission);
|
|
177
|
+
const query = optionalString(input.q ?? input.query)?.toLowerCase();
|
|
178
|
+
const operations = publicComputerOperationRegistry().filter((operation) => (matchesComputerOperationCategory(operation, category) &&
|
|
179
|
+
matchesComputerOperationPermission(operation, permission) &&
|
|
180
|
+
matchesComputerOperationQuery(operation, query)));
|
|
181
|
+
return {
|
|
182
|
+
kind: "computer-operation-registry",
|
|
183
|
+
schemaVersion: 1,
|
|
184
|
+
contract: computerOperationContract,
|
|
185
|
+
filters: {
|
|
186
|
+
contract: "computer",
|
|
187
|
+
category,
|
|
188
|
+
permission,
|
|
189
|
+
query,
|
|
190
|
+
},
|
|
191
|
+
count: operations.length,
|
|
192
|
+
operations,
|
|
193
|
+
compatibility: {
|
|
194
|
+
workspaceRegistry: {
|
|
195
|
+
action: "operation_registry",
|
|
196
|
+
input: { contract: "workspace" },
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function workspaceOperationRegistryData(input) {
|
|
202
|
+
const category = optionalString(input.category);
|
|
203
|
+
const permission = optionalString(input.permission);
|
|
204
|
+
const query = optionalString(input.q ?? input.query)?.toLowerCase();
|
|
205
|
+
const operations = publicWorkspaceOperationRegistry(workspaceOperationRegistry.filter((operation) => (matchesOperationCategory(operation, category) &&
|
|
206
|
+
matchesOperationPermission(operation, permission) &&
|
|
207
|
+
matchesOperationQuery(operation, query))));
|
|
208
|
+
return {
|
|
209
|
+
kind: "operation-registry",
|
|
210
|
+
schemaVersion: 1,
|
|
211
|
+
contract: workspaceOperationContract,
|
|
212
|
+
filters: {
|
|
213
|
+
contract: "workspace",
|
|
214
|
+
category,
|
|
215
|
+
permission,
|
|
216
|
+
query,
|
|
217
|
+
},
|
|
218
|
+
count: operations.length,
|
|
219
|
+
operations,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function matchesComputerOperationCategory(operation, category) {
|
|
223
|
+
if (!category)
|
|
224
|
+
return true;
|
|
225
|
+
if (operation.category === category)
|
|
226
|
+
return true;
|
|
227
|
+
if (category === "search")
|
|
228
|
+
return operation.op === "file.search" || operation.op === "code.search_symbols";
|
|
229
|
+
if (category === "coding")
|
|
230
|
+
return operation.category === "code";
|
|
231
|
+
if (category === "files")
|
|
232
|
+
return operation.category === "file";
|
|
233
|
+
if (category === "metadata")
|
|
234
|
+
return operation.category === "history";
|
|
235
|
+
return false;
|
|
236
|
+
}
|
|
237
|
+
function matchesComputerOperationPermission(operation, permission) {
|
|
238
|
+
return !permission || operation.permission === permission;
|
|
239
|
+
}
|
|
240
|
+
function matchesComputerOperationQuery(operation, query) {
|
|
241
|
+
if (!query)
|
|
242
|
+
return true;
|
|
243
|
+
return [
|
|
244
|
+
operation.op,
|
|
245
|
+
operation.category,
|
|
246
|
+
operation.permission,
|
|
247
|
+
operation.description,
|
|
248
|
+
operation.boundary,
|
|
249
|
+
operation.target ?? "",
|
|
250
|
+
operation.backendOperation,
|
|
251
|
+
operation.legacyWorkspaceOperation,
|
|
252
|
+
...operation.capabilities,
|
|
253
|
+
...operation.requiredInput,
|
|
254
|
+
...operation.optionalInput,
|
|
255
|
+
...operation.options,
|
|
256
|
+
].some((value) => value.toLowerCase().includes(query));
|
|
257
|
+
}
|
|
258
|
+
function matchesOperationCategory(operation, category) {
|
|
259
|
+
return !category || operation.category === category;
|
|
260
|
+
}
|
|
261
|
+
function matchesOperationPermission(operation, permission) {
|
|
262
|
+
return !permission || operation.permission === permission;
|
|
263
|
+
}
|
|
264
|
+
function matchesOperationQuery(operation, query) {
|
|
265
|
+
if (!query)
|
|
266
|
+
return true;
|
|
267
|
+
return [
|
|
268
|
+
operation.operation,
|
|
269
|
+
operation.name,
|
|
270
|
+
operation.category,
|
|
271
|
+
operation.permission,
|
|
272
|
+
operation.description,
|
|
273
|
+
operation.boundary,
|
|
274
|
+
...operation.capabilities,
|
|
275
|
+
...operation.requiredFields,
|
|
276
|
+
...operation.optionalFields,
|
|
277
|
+
].some((value) => value.toLowerCase().includes(query));
|
|
278
|
+
}
|
|
279
|
+
async function auditedApiCall(tool, fields, run, success) {
|
|
280
|
+
const startedAt = performance.now();
|
|
281
|
+
try {
|
|
282
|
+
const result = await run();
|
|
283
|
+
writeAuditEvent({
|
|
284
|
+
type: "tool_call",
|
|
285
|
+
tool,
|
|
286
|
+
success: success ? success(result) : true,
|
|
287
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
288
|
+
...fields,
|
|
289
|
+
});
|
|
290
|
+
return result;
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
writeAuditEvent({
|
|
294
|
+
type: "tool_call",
|
|
295
|
+
tool,
|
|
296
|
+
success: false,
|
|
297
|
+
durationMs: Math.round(performance.now() - startedAt),
|
|
298
|
+
error: errorMessage(error),
|
|
299
|
+
...fields,
|
|
300
|
+
});
|
|
301
|
+
throw error;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function operationResultSucceeded(result) {
|
|
305
|
+
return !(result && typeof result === "object" && result.ok === false);
|
|
306
|
+
}
|
|
307
|
+
function sendApiError(res, error) {
|
|
308
|
+
const status = error instanceof PermissionDeniedError ? 403 : 400;
|
|
309
|
+
res.status(status).json({ ok: false, error: errorMessage(error) });
|
|
310
|
+
}
|
|
311
|
+
function requiredString(value, name) {
|
|
312
|
+
const text = optionalString(value);
|
|
313
|
+
if (!text)
|
|
314
|
+
throw new Error(`${name} is required`);
|
|
315
|
+
return text;
|
|
316
|
+
}
|
|
317
|
+
function optionalString(value) {
|
|
318
|
+
const text = String(value ?? "").trim();
|
|
319
|
+
return text || undefined;
|
|
320
|
+
}
|
|
321
|
+
function optionalPositiveInteger(value) {
|
|
322
|
+
return optionalBoundedPositiveInteger(value, 1000);
|
|
323
|
+
}
|
|
324
|
+
function optionalBoundedPositiveInteger(value, max) {
|
|
325
|
+
const text = optionalString(value);
|
|
326
|
+
if (!text)
|
|
327
|
+
return undefined;
|
|
328
|
+
const parsed = Number.parseInt(text, 10);
|
|
329
|
+
return Number.isFinite(parsed) && parsed > 0 ? Math.min(parsed, max) : undefined;
|
|
330
|
+
}
|
|
331
|
+
function optionalBoundedNonNegativeInteger(value, max) {
|
|
332
|
+
const text = optionalString(value);
|
|
333
|
+
if (!text)
|
|
334
|
+
return undefined;
|
|
335
|
+
const parsed = Number.parseInt(text, 10);
|
|
336
|
+
return Number.isFinite(parsed) && parsed >= 0 ? Math.min(parsed, max) : undefined;
|
|
337
|
+
}
|
|
338
|
+
function optionalBoolean(value) {
|
|
339
|
+
if (value === true || value === "true" || value === "on" || value === "1")
|
|
340
|
+
return true;
|
|
341
|
+
if (value === false || value === "false" || value === "off" || value === "0")
|
|
342
|
+
return false;
|
|
343
|
+
return undefined;
|
|
344
|
+
}
|
|
345
|
+
function workspaceOperationInput(body) {
|
|
346
|
+
return normalizeWorkspaceOperationInput(body);
|
|
347
|
+
}
|
|
348
|
+
function auditType(value) {
|
|
349
|
+
const text = optionalString(value);
|
|
350
|
+
return text === "tool_call" || text === "workspace_open" || text === "mcp_session" || text === "auth_failure" || text === "admin_action" ? text : undefined;
|
|
351
|
+
}
|
|
352
|
+
function optionalStringArray(value) {
|
|
353
|
+
if (!Array.isArray(value))
|
|
354
|
+
return undefined;
|
|
355
|
+
const paths = value.map(optionalString).filter((path) => Boolean(path));
|
|
356
|
+
return paths.length ? paths.slice(0, 100) : undefined;
|
|
357
|
+
}
|
|
358
|
+
function requestPath(req) {
|
|
359
|
+
return `${req.baseUrl}${req.path}`;
|
|
360
|
+
}
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface AuditEvent {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
type: "tool_call" | "workspace_open" | "mcp_session" | "auth_failure" | "admin_action" | "tunnel_event";
|
|
4
|
+
success: boolean;
|
|
5
|
+
durationMs?: number;
|
|
6
|
+
tool?: string;
|
|
7
|
+
workspaceId?: string;
|
|
8
|
+
workspaceRoot?: string;
|
|
9
|
+
workspaceRef?: string;
|
|
10
|
+
path?: string;
|
|
11
|
+
requestPath?: string;
|
|
12
|
+
remoteAddress?: string;
|
|
13
|
+
workingDirectory?: string;
|
|
14
|
+
commandPreview?: string;
|
|
15
|
+
operation?: string;
|
|
16
|
+
target?: string;
|
|
17
|
+
detail?: string;
|
|
18
|
+
replay?: AuditReplayTemplate;
|
|
19
|
+
error?: string;
|
|
20
|
+
provider?: string;
|
|
21
|
+
tunnelId?: string;
|
|
22
|
+
externalSessionId?: string;
|
|
23
|
+
requestId?: string;
|
|
24
|
+
cmdRequestId?: string;
|
|
25
|
+
rpcRequestId?: string;
|
|
26
|
+
tunnelRequestId?: string;
|
|
27
|
+
severity?: "info" | "warn" | "error";
|
|
28
|
+
statusCode?: number;
|
|
29
|
+
}
|
|
30
|
+
export interface AuditReplayTemplate {
|
|
31
|
+
action: "workspace_operation";
|
|
32
|
+
replayable: boolean;
|
|
33
|
+
reason?: string;
|
|
34
|
+
requiresInput?: string[];
|
|
35
|
+
input: {
|
|
36
|
+
op: string;
|
|
37
|
+
target?: string;
|
|
38
|
+
input: Record<string, unknown>;
|
|
39
|
+
options: Record<string, unknown>;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
export type AuditEventInput = Omit<AuditEvent, "timestamp">;
|
|
43
|
+
export interface ReadAuditEventsOptions {
|
|
44
|
+
limit?: number;
|
|
45
|
+
type?: AuditEvent["type"];
|
|
46
|
+
success?: boolean;
|
|
47
|
+
tool?: string;
|
|
48
|
+
workspaceId?: string;
|
|
49
|
+
query?: string;
|
|
50
|
+
}
|
|
51
|
+
export declare function writeAuditEvent(event: AuditEventInput): void;
|
|
52
|
+
export declare function writeAuthFailureEvent(input: {
|
|
53
|
+
surface: "api" | "mcp";
|
|
54
|
+
method: string;
|
|
55
|
+
requestPath: string;
|
|
56
|
+
remoteAddress?: string;
|
|
57
|
+
detail?: string;
|
|
58
|
+
}): void;
|
|
59
|
+
export declare function writeAdminActionEvent(input: {
|
|
60
|
+
action: string;
|
|
61
|
+
success?: boolean;
|
|
62
|
+
workspaceId?: string;
|
|
63
|
+
path?: string;
|
|
64
|
+
detail?: string;
|
|
65
|
+
error?: string;
|
|
66
|
+
}): void;
|
|
67
|
+
export declare function readRecentAuditEvents(limit?: number): AuditEvent[];
|
|
68
|
+
export declare function readAuditEvents(options?: ReadAuditEventsOptions): AuditEvent[];
|
|
69
|
+
export declare function errorMessage(error: unknown): string;
|
|
70
|
+
export declare function previewCommand(command: string): string;
|
package/dist/audit.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { auditLogPath } from "./config.js";
|
|
4
|
+
export function writeAuditEvent(event) {
|
|
5
|
+
const path = auditLogPath();
|
|
6
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
7
|
+
appendFileSync(path, `${JSON.stringify({ timestamp: new Date().toISOString(), ...event })}\n`, {
|
|
8
|
+
mode: 0o600,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
export function writeAuthFailureEvent(input) {
|
|
12
|
+
writeAuditEvent({
|
|
13
|
+
type: "auth_failure",
|
|
14
|
+
success: false,
|
|
15
|
+
tool: input.surface,
|
|
16
|
+
requestPath: input.requestPath,
|
|
17
|
+
remoteAddress: input.remoteAddress,
|
|
18
|
+
detail: input.detail ?? "unauthorized",
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
export function writeAdminActionEvent(input) {
|
|
22
|
+
const detail = input.detail && input.detail !== input.action
|
|
23
|
+
? `${input.action}: ${input.detail}`
|
|
24
|
+
: input.action;
|
|
25
|
+
writeAuditEvent({
|
|
26
|
+
type: "admin_action",
|
|
27
|
+
success: input.success ?? true,
|
|
28
|
+
tool: "cli",
|
|
29
|
+
workspaceId: input.workspaceId,
|
|
30
|
+
path: input.path,
|
|
31
|
+
detail,
|
|
32
|
+
error: input.error,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export function readRecentAuditEvents(limit = 50) {
|
|
36
|
+
return readAuditEvents({ limit });
|
|
37
|
+
}
|
|
38
|
+
export function readAuditEvents(options = {}) {
|
|
39
|
+
const path = auditLogPath();
|
|
40
|
+
if (!existsSync(path))
|
|
41
|
+
return [];
|
|
42
|
+
let events = readFileSync(path, "utf8")
|
|
43
|
+
.trimEnd()
|
|
44
|
+
.split("\n")
|
|
45
|
+
.filter(Boolean)
|
|
46
|
+
.map((line) => JSON.parse(line));
|
|
47
|
+
if (options.type)
|
|
48
|
+
events = events.filter((event) => event.type === options.type);
|
|
49
|
+
if (options.success !== undefined)
|
|
50
|
+
events = events.filter((event) => event.success === options.success);
|
|
51
|
+
if (options.tool)
|
|
52
|
+
events = events.filter((event) => event.tool === options.tool);
|
|
53
|
+
if (options.workspaceId) {
|
|
54
|
+
events = events.filter((event) => event.workspaceId === options.workspaceId || event.workspaceRef === options.workspaceId);
|
|
55
|
+
}
|
|
56
|
+
if (options.query) {
|
|
57
|
+
const query = options.query.toLowerCase();
|
|
58
|
+
events = events.filter((event) => auditSearchText(event).includes(query));
|
|
59
|
+
}
|
|
60
|
+
if (options.limit && options.limit > 0) {
|
|
61
|
+
events = events.slice(Math.max(0, events.length - options.limit));
|
|
62
|
+
}
|
|
63
|
+
return events.reverse();
|
|
64
|
+
}
|
|
65
|
+
function auditSearchText(event) {
|
|
66
|
+
return [
|
|
67
|
+
event.timestamp,
|
|
68
|
+
event.type,
|
|
69
|
+
event.tool,
|
|
70
|
+
event.workspaceId,
|
|
71
|
+
event.workspaceRoot,
|
|
72
|
+
event.workspaceRef,
|
|
73
|
+
event.path,
|
|
74
|
+
event.requestPath,
|
|
75
|
+
event.remoteAddress,
|
|
76
|
+
event.workingDirectory,
|
|
77
|
+
event.commandPreview,
|
|
78
|
+
event.operation,
|
|
79
|
+
event.target,
|
|
80
|
+
event.detail,
|
|
81
|
+
event.error,
|
|
82
|
+
event.provider,
|
|
83
|
+
event.tunnelId,
|
|
84
|
+
event.externalSessionId,
|
|
85
|
+
event.requestId,
|
|
86
|
+
event.cmdRequestId,
|
|
87
|
+
event.rpcRequestId,
|
|
88
|
+
event.tunnelRequestId,
|
|
89
|
+
event.severity,
|
|
90
|
+
event.statusCode === undefined ? undefined : String(event.statusCode),
|
|
91
|
+
]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join("\n")
|
|
94
|
+
.toLowerCase();
|
|
95
|
+
}
|
|
96
|
+
export function errorMessage(error) {
|
|
97
|
+
return error instanceof Error ? error.message : String(error);
|
|
98
|
+
}
|
|
99
|
+
export function previewCommand(command) {
|
|
100
|
+
const normalized = command.replace(/\s+/g, " ").trim();
|
|
101
|
+
return normalized.length > 160 ? `${normalized.slice(0, 157)}...` : normalized;
|
|
102
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type ConfigDiagnostic } from "./config-diagnostics.js";
|
|
2
|
+
import type { LocalPortConfig } from "./permissions.js";
|
|
3
|
+
import { securityDiagnostics } from "./security.js";
|
|
4
|
+
import { type ServiceStatus } from "./service.js";
|
|
5
|
+
export interface CommandCapability {
|
|
6
|
+
name: string;
|
|
7
|
+
category: "agent" | "search" | "runtime" | "package-manager" | "vcs" | "shell" | "container";
|
|
8
|
+
importance: "required" | "recommended" | "optional";
|
|
9
|
+
available: boolean;
|
|
10
|
+
path?: string;
|
|
11
|
+
version?: string;
|
|
12
|
+
usedFor: string[];
|
|
13
|
+
install?: ToolInstallHint;
|
|
14
|
+
error?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ToolInstallHint {
|
|
17
|
+
macos?: string;
|
|
18
|
+
linux?: string;
|
|
19
|
+
windows?: string;
|
|
20
|
+
docs?: string;
|
|
21
|
+
}
|
|
22
|
+
export interface ToolReadiness {
|
|
23
|
+
kind: "computer-linker-tool-readiness";
|
|
24
|
+
schemaVersion: 1;
|
|
25
|
+
ready: boolean;
|
|
26
|
+
requiredMissing: string[];
|
|
27
|
+
recommendedMissing: string[];
|
|
28
|
+
availableRecommended: string[];
|
|
29
|
+
installHints: Array<{
|
|
30
|
+
name: string;
|
|
31
|
+
importance: CommandCapability["importance"];
|
|
32
|
+
usedFor: string[];
|
|
33
|
+
install?: ToolInstallHint;
|
|
34
|
+
}>;
|
|
35
|
+
}
|
|
36
|
+
export interface StartupReadinessCheck {
|
|
37
|
+
id: string;
|
|
38
|
+
status: "pass" | "warn" | "fail";
|
|
39
|
+
message: string;
|
|
40
|
+
detail?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface StartupReadinessMode {
|
|
43
|
+
id: "start" | "tunnel-cloudflare" | "tunnel-tailscale" | "tunnel-openai" | "stdio" | "service";
|
|
44
|
+
title: string;
|
|
45
|
+
command: string;
|
|
46
|
+
persistent: boolean;
|
|
47
|
+
useWhen: string;
|
|
48
|
+
}
|
|
49
|
+
export interface StartupReadiness {
|
|
50
|
+
kind: "computer-linker-startup-readiness";
|
|
51
|
+
schemaVersion: 1;
|
|
52
|
+
ready: boolean;
|
|
53
|
+
platform: string;
|
|
54
|
+
recommendedMode: StartupReadinessMode["id"];
|
|
55
|
+
localMcpUrl: string;
|
|
56
|
+
localApiUrl: string;
|
|
57
|
+
modes: StartupReadinessMode[];
|
|
58
|
+
service: {
|
|
59
|
+
platform: string;
|
|
60
|
+
serviceName: string;
|
|
61
|
+
label: string;
|
|
62
|
+
command: string;
|
|
63
|
+
manifestPath: string;
|
|
64
|
+
manifestExists: boolean | null;
|
|
65
|
+
statusCommands: string[];
|
|
66
|
+
profileCommand: string;
|
|
67
|
+
profileBundleCommand: string;
|
|
68
|
+
installDryRunCommand: string;
|
|
69
|
+
uninstallDryRunCommand: string;
|
|
70
|
+
};
|
|
71
|
+
checks: StartupReadinessCheck[];
|
|
72
|
+
nextActions: string[];
|
|
73
|
+
}
|
|
74
|
+
export interface ReleaseReadinessCheck {
|
|
75
|
+
id: string;
|
|
76
|
+
status: "pass" | "warn" | "fail";
|
|
77
|
+
message: string;
|
|
78
|
+
detail?: string;
|
|
79
|
+
}
|
|
80
|
+
export interface ReleaseReadiness {
|
|
81
|
+
kind: "computer-linker-release-readiness";
|
|
82
|
+
schemaVersion: 1;
|
|
83
|
+
ready: boolean;
|
|
84
|
+
status: "ready" | "needs_attention" | "blocked";
|
|
85
|
+
checks: ReleaseReadinessCheck[];
|
|
86
|
+
blockingReasons: string[];
|
|
87
|
+
warnings: string[];
|
|
88
|
+
recommendedGate: string;
|
|
89
|
+
}
|
|
90
|
+
export declare function getLocalPortCapabilities(): unknown;
|
|
91
|
+
export declare function getLocalPortDoctor(): unknown;
|
|
92
|
+
export declare function startupReadiness(config: LocalPortConfig, service?: ServiceStatus): StartupReadiness;
|
|
93
|
+
export declare function releaseReadiness(config: LocalPortConfig, input: {
|
|
94
|
+
toolReadiness: ToolReadiness;
|
|
95
|
+
startup: StartupReadiness;
|
|
96
|
+
configFindings: ConfigDiagnostic[];
|
|
97
|
+
securityFindings: ReturnType<typeof securityDiagnostics>;
|
|
98
|
+
}): ReleaseReadiness;
|