@cursorpool-dev/cli 0.5.6
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/bin/cursor-pool.mjs +9 -0
- package/bin/cursor-pool.ts +169 -0
- package/node_modules/@cursor-pool/extension/dist/extension.js +2910 -0
- package/node_modules/@cursor-pool/extension/package.json +64 -0
- package/node_modules/@cursor-pool/extension/resources/cursor-pool.svg +6 -0
- package/node_modules/@cursor-pool/extension/src/api.ts +545 -0
- package/node_modules/@cursor-pool/extension/src/extension.ts +104 -0
- package/node_modules/@cursor-pool/extension/src/index.ts +1 -0
- package/node_modules/@cursor-pool/extension/src/panel.ts +569 -0
- package/node_modules/@cursor-pool/extension/src/runtime.ts +22 -0
- package/node_modules/@cursor-pool/extension/test/panel.test.ts +1785 -0
- package/node_modules/@cursor-pool/patcher/package.json +17 -0
- package/node_modules/@cursor-pool/patcher/src/alwaysLocalMarker.ts +86 -0
- package/node_modules/@cursor-pool/patcher/src/hash.ts +7 -0
- package/node_modules/@cursor-pool/patcher/src/index.ts +55 -0
- package/node_modules/@cursor-pool/patcher/src/marker.ts +159 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorAgentExec.ts +154 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorAlwaysLocal.ts +142 -0
- package/node_modules/@cursor-pool/patcher/src/patchCursorWorkbenchAuthGate.ts +140 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorAgentExec.ts +52 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorAlwaysLocal.ts +52 -0
- package/node_modules/@cursor-pool/patcher/src/restoreCursorWorkbenchAuthGate.ts +70 -0
- package/node_modules/@cursor-pool/patcher/src/workbenchAuthGateMarker.ts +243 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorAgentExec.test.ts +630 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorAlwaysLocal.test.ts +144 -0
- package/node_modules/@cursor-pool/patcher/test/patchCursorWorkbench.test.ts +770 -0
- package/node_modules/@cursor-pool/patcher/test/restoreCursorAgentExec.test.ts +139 -0
- package/node_modules/@cursor-pool/service/package.json +17 -0
- package/node_modules/@cursor-pool/service/src/canary.ts +61 -0
- package/node_modules/@cursor-pool/service/src/diagnostics.ts +385 -0
- package/node_modules/@cursor-pool/service/src/entry.ts +161 -0
- package/node_modules/@cursor-pool/service/src/health.ts +10 -0
- package/node_modules/@cursor-pool/service/src/index.ts +29 -0
- package/node_modules/@cursor-pool/service/src/metadata.ts +22 -0
- package/node_modules/@cursor-pool/service/src/platformSession.ts +1178 -0
- package/node_modules/@cursor-pool/service/src/requestCheck.ts +81 -0
- package/node_modules/@cursor-pool/service/src/requestGate.ts +100 -0
- package/node_modules/@cursor-pool/service/src/requestGateway.ts +441 -0
- package/node_modules/@cursor-pool/service/src/runtime.ts +48 -0
- package/node_modules/@cursor-pool/service/src/server.ts +939 -0
- package/node_modules/@cursor-pool/service/src/takeover.ts +111 -0
- package/node_modules/@cursor-pool/service/test/canary.test.ts +140 -0
- package/node_modules/@cursor-pool/service/test/diagnostics.test.ts +506 -0
- package/node_modules/@cursor-pool/service/test/metadata.test.ts +63 -0
- package/node_modules/@cursor-pool/service/test/platformSession.test.ts +2428 -0
- package/node_modules/@cursor-pool/service/test/requestCheck.test.ts +152 -0
- package/node_modules/@cursor-pool/service/test/requestGate.test.ts +207 -0
- package/node_modules/@cursor-pool/service/test/requestGateway.test.ts +466 -0
- package/node_modules/@cursor-pool/service/test/runtime.test.ts +47 -0
- package/node_modules/@cursor-pool/service/test/server.test.ts +2570 -0
- package/node_modules/@cursor-pool/shared/package.json +17 -0
- package/node_modules/@cursor-pool/shared/src/clientConfig.ts +49 -0
- package/node_modules/@cursor-pool/shared/src/index.ts +14 -0
- package/node_modules/@cursor-pool/shared/src/manifest.ts +36 -0
- package/node_modules/@cursor-pool/shared/src/metadata.ts +19 -0
- package/node_modules/@cursor-pool/shared/src/paths.ts +5 -0
- package/node_modules/@cursor-pool/shared/src/runtime.ts +3 -0
- package/node_modules/@cursor-pool/shared/test/index.test.ts +56 -0
- package/node_modules/@cursor-pool/shared/test/manifest.test.ts +65 -0
- package/node_modules/@cursor-pool/shared/test/metadata.test.ts +25 -0
- package/node_modules/@cursor-pool/shared/test/runtime.test.ts +8 -0
- package/package.json +28 -0
- package/src/adHocResign.ts +65 -0
- package/src/autostart.ts +240 -0
- package/src/compat.ts +282 -0
- package/src/confirm.ts +76 -0
- package/src/cursor.ts +94 -0
- package/src/diagnostics.ts +558 -0
- package/src/environment.ts +18 -0
- package/src/extensionBundle.ts +111 -0
- package/src/extensionLink.ts +168 -0
- package/src/index.ts +23 -0
- package/src/install.ts +614 -0
- package/src/installRecord.ts +105 -0
- package/src/launch.ts +182 -0
- package/src/patchSet.ts +182 -0
- package/src/platform.ts +132 -0
- package/src/repair.ts +383 -0
- package/src/restore.ts +153 -0
- package/src/serviceCommands.ts +79 -0
- package/src/serviceProcess.ts +188 -0
- package/src/status.ts +241 -0
- package/src/target.ts +37 -0
- package/src/trial.ts +133 -0
- package/src/uninstall.ts +213 -0
- package/test/autostart.test.ts +151 -0
- package/test/compat.test.ts +192 -0
- package/test/confirm.test.ts +114 -0
- package/test/cursor-pool-bin.test.ts +658 -0
- package/test/cursor.test.ts +20 -0
- package/test/diagnostics.test.ts +709 -0
- package/test/e2e-install.test.ts +773 -0
- package/test/extensionBundle.test.ts +161 -0
- package/test/extensionLink.test.ts +209 -0
- package/test/install.test.ts +862 -0
- package/test/installRecord.test.ts +107 -0
- package/test/launch.test.ts +138 -0
- package/test/platform.test.ts +226 -0
- package/test/repair.test.ts +575 -0
- package/test/restore.test.ts +211 -0
- package/test/serviceCommands.test.ts +135 -0
- package/test/serviceProcess.test.ts +280 -0
- package/test/status.test.ts +615 -0
- package/test/target.test.ts +49 -0
- package/test/trial.test.ts +146 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { sha256File } from '../src/hash';
|
|
7
|
+
import {
|
|
8
|
+
buildCursorAgentExecPatchSnippet,
|
|
9
|
+
CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR,
|
|
10
|
+
CURSOR_POOL_PATCH_MARKER,
|
|
11
|
+
hasCursorPoolMarker,
|
|
12
|
+
} from '../src/marker';
|
|
13
|
+
import {
|
|
14
|
+
backupPathForCursorAgentExec,
|
|
15
|
+
patchCursorAgentExec,
|
|
16
|
+
} from '../src/patchCursorAgentExec';
|
|
17
|
+
import { restoreCursorAgentExec } from '../src/restoreCursorAgentExec';
|
|
18
|
+
|
|
19
|
+
const targetRelativePath =
|
|
20
|
+
'Contents/Resources/app/extensions/cursor-agent-exec/dist/main.js';
|
|
21
|
+
|
|
22
|
+
async function createFixture(
|
|
23
|
+
content = `const agent = "cursor-agent-exec";\n${CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR}\nexport { agent };\n`,
|
|
24
|
+
) {
|
|
25
|
+
const base = await mkdtemp(join(tmpdir(), 'cursor-pool-restore-'));
|
|
26
|
+
const appPath = join(base, `Fixture-${Date.now()}-${Math.random()}.app`);
|
|
27
|
+
await mkdir(join(appPath, 'Contents/Resources/app/extensions/cursor-agent-exec/dist'), {
|
|
28
|
+
recursive: true,
|
|
29
|
+
});
|
|
30
|
+
await writeFile(join(appPath, targetRelativePath), content, 'utf8');
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
appPath,
|
|
34
|
+
targetPath: join(appPath, targetRelativePath),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
test('restore returns the patched target hash to its original value and removes marker', async () => {
|
|
39
|
+
const tempDir = join(tmpdir(), `cursor-pool-restore-backups-${Date.now()}-${Math.random()}`);
|
|
40
|
+
const fixture = await createFixture();
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const beforeHash = await sha256File(fixture.targetPath);
|
|
44
|
+
const patch = await patchCursorAgentExec(fixture.appPath, { backupDir: tempDir });
|
|
45
|
+
assert.equal(await hasCursorPoolMarker(fixture.targetPath), true);
|
|
46
|
+
|
|
47
|
+
const restore = await restoreCursorAgentExec(fixture.appPath, { backupDir: tempDir });
|
|
48
|
+
|
|
49
|
+
const afterRestoreHash = await sha256File(fixture.targetPath);
|
|
50
|
+
const markerPresent = await hasCursorPoolMarker(fixture.targetPath);
|
|
51
|
+
|
|
52
|
+
assert.equal(patch.beforeHash, beforeHash);
|
|
53
|
+
assert.equal(restore.beforeHash, patch.afterHash);
|
|
54
|
+
assert.equal(afterRestoreHash, beforeHash);
|
|
55
|
+
assert.equal(restore.afterHash, beforeHash);
|
|
56
|
+
assert.equal(markerPresent, false);
|
|
57
|
+
} finally {
|
|
58
|
+
await rm(fixture.appPath, { recursive: true, force: true });
|
|
59
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('restore supports a custom Linux Cursor agent exec relative path', async () => {
|
|
64
|
+
const tempDir = join(tmpdir(), `cursor-pool-restore-linux-path-${Date.now()}-${Math.random()}`);
|
|
65
|
+
const appPath = join(tempDir, 'squashfs-root');
|
|
66
|
+
const customTargetRelativePath =
|
|
67
|
+
'usr/share/cursor/resources/app/extensions/cursor-agent-exec/dist/main.js';
|
|
68
|
+
const targetPath = join(appPath, customTargetRelativePath);
|
|
69
|
+
const originalContent = `const agent = "cursor-agent-exec";\n${CURSOR_POOL_AGENT_EXEC_PROVIDER_REGISTER_ANCHOR}\nexport { agent };\n`;
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
73
|
+
await writeFile(targetPath, originalContent, 'utf8');
|
|
74
|
+
|
|
75
|
+
await patchCursorAgentExec(appPath, {
|
|
76
|
+
backupDir: tempDir,
|
|
77
|
+
targetRelativePath: customTargetRelativePath,
|
|
78
|
+
});
|
|
79
|
+
const restore = await restoreCursorAgentExec(appPath, {
|
|
80
|
+
backupDir: tempDir,
|
|
81
|
+
targetRelativePath: customTargetRelativePath,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
assert.equal(restore.targetPath, targetPath);
|
|
85
|
+
assert.equal(await readFile(targetPath, 'utf8'), originalContent);
|
|
86
|
+
} finally {
|
|
87
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('restore refuses a backup that contains a valid patch marker and leaves target unchanged', async () => {
|
|
92
|
+
const tempDir = join(tmpdir(), `cursor-pool-restore-tampered-backups-${Date.now()}-${Math.random()}`);
|
|
93
|
+
const fixture = await createFixture();
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await patchCursorAgentExec(fixture.appPath, { backupDir: tempDir });
|
|
97
|
+
const patchedHash = await sha256File(fixture.targetPath);
|
|
98
|
+
const backupPath = await backupPathForCursorAgentExec(fixture.appPath, fixture.targetPath, tempDir);
|
|
99
|
+
await writeFile(backupPath, `${await readFile(fixture.targetPath, 'utf8')}`, 'utf8');
|
|
100
|
+
|
|
101
|
+
await assert.rejects(
|
|
102
|
+
restoreCursorAgentExec(fixture.appPath, { backupDir: tempDir }),
|
|
103
|
+
/backup.*patch marker/i,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
assert.equal(await sha256File(fixture.targetPath), patchedHash);
|
|
107
|
+
assert.equal(await hasCursorPoolMarker(fixture.targetPath), true);
|
|
108
|
+
} finally {
|
|
109
|
+
await rm(fixture.appPath, { recursive: true, force: true });
|
|
110
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('restore refuses a backup that would leave marker text behind', async () => {
|
|
115
|
+
const tempDir = join(tmpdir(), `cursor-pool-restore-partial-marker-${Date.now()}-${Math.random()}`);
|
|
116
|
+
const fixture = await createFixture();
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await patchCursorAgentExec(fixture.appPath, { backupDir: tempDir });
|
|
120
|
+
const patchedHash = await sha256File(fixture.targetPath);
|
|
121
|
+
const backupPath = await backupPathForCursorAgentExec(fixture.appPath, fixture.targetPath, tempDir);
|
|
122
|
+
await writeFile(
|
|
123
|
+
backupPath,
|
|
124
|
+
`${await readFile(fixture.targetPath, 'utf8')}\n/* stray ${CURSOR_POOL_PATCH_MARKER}: begin */\n`,
|
|
125
|
+
'utf8',
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
await assert.rejects(
|
|
129
|
+
restoreCursorAgentExec(fixture.appPath, { backupDir: tempDir }),
|
|
130
|
+
/restore.*marker/i,
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
assert.equal(await sha256File(fixture.targetPath), patchedHash);
|
|
134
|
+
assert.equal(await hasCursorPoolMarker(fixture.targetPath), true);
|
|
135
|
+
} finally {
|
|
136
|
+
await rm(fixture.appPath, { recursive: true, force: true });
|
|
137
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cursor-pool/service",
|
|
3
|
+
"version": "0.5.6",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./src/index.ts",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": "./src/index.ts",
|
|
8
|
+
"./entry": "./src/entry.ts",
|
|
9
|
+
"./platformSession": "./src/platformSession.ts"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@cursor-pool/shared": "0.5.6"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "tsx --test test/*.test.ts"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { RequestGateDecision } from './requestGate';
|
|
3
|
+
|
|
4
|
+
export type AgentCanary = {
|
|
5
|
+
requestId: string;
|
|
6
|
+
requestType: 'agent';
|
|
7
|
+
source: 'cursor-agent-exec';
|
|
8
|
+
model: string;
|
|
9
|
+
receivedAt: string;
|
|
10
|
+
runtimeId: string;
|
|
11
|
+
gate: RequestGateDecision;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type SanitizeAgentCanaryOptions = {
|
|
15
|
+
runtimeId: string;
|
|
16
|
+
gate: RequestGateDecision;
|
|
17
|
+
now?: () => string;
|
|
18
|
+
requestId?: () => string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type CanaryInput = {
|
|
22
|
+
requestId?: unknown;
|
|
23
|
+
model?: unknown;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const isValidRequestId = (value: unknown): value is string =>
|
|
27
|
+
typeof value === 'string' && value.length > 0 && value.length <= 128;
|
|
28
|
+
|
|
29
|
+
export function sanitizeAgentCanary(
|
|
30
|
+
input: CanaryInput,
|
|
31
|
+
options: SanitizeAgentCanaryOptions,
|
|
32
|
+
): AgentCanary {
|
|
33
|
+
const requestId = isValidRequestId(input.requestId)
|
|
34
|
+
? input.requestId
|
|
35
|
+
: (options.requestId ?? randomUUID)();
|
|
36
|
+
const model = typeof input.model === 'string' && input.model.length > 0 ? input.model : 'unknown';
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
requestId,
|
|
40
|
+
requestType: 'agent',
|
|
41
|
+
source: 'cursor-agent-exec',
|
|
42
|
+
model,
|
|
43
|
+
receivedAt: (options.now ?? (() => new Date().toISOString()))(),
|
|
44
|
+
runtimeId: options.runtimeId,
|
|
45
|
+
gate: options.gate,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createCanaryStore() {
|
|
50
|
+
let latestCanary: AgentCanary | null = null;
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
record(canary: AgentCanary): AgentCanary {
|
|
54
|
+
latestCanary = canary;
|
|
55
|
+
return canary;
|
|
56
|
+
},
|
|
57
|
+
latest(): AgentCanary | null {
|
|
58
|
+
return latestCanary;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { DEFAULT_DIAGNOSTICS_FILE } from '@cursor-pool/shared/runtime';
|
|
5
|
+
import type { AgentCanary } from './canary';
|
|
6
|
+
import type { AgentRequestCheck } from './requestCheck';
|
|
7
|
+
import { sanitizeGatewayForward } from './requestGateway';
|
|
8
|
+
import type { AgentGateway, AgentGatewayDecision, SafeGatewayForwardState } from './requestGateway';
|
|
9
|
+
import type { RequestGateDecision } from './requestGate';
|
|
10
|
+
import type { SafeRouteState } from './platformSession';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MAX_DIAGNOSTICS = 20;
|
|
13
|
+
|
|
14
|
+
type DiagnosticOptions = {
|
|
15
|
+
diagnosticsFile?: string;
|
|
16
|
+
maxEntries?: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type AgentCanaryDiagnostic = {
|
|
20
|
+
requestId: string;
|
|
21
|
+
requestType: 'agent';
|
|
22
|
+
source: 'cursor-agent-exec';
|
|
23
|
+
model: string;
|
|
24
|
+
receivedAt: string;
|
|
25
|
+
runtimeId: string;
|
|
26
|
+
gate: RequestGateDecision;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type AgentRequestCheckDiagnostic = {
|
|
30
|
+
kind: 'agent-request-check';
|
|
31
|
+
requestId: string;
|
|
32
|
+
requestType: 'agent';
|
|
33
|
+
source: 'cursor-agent-exec' | 'manual-check';
|
|
34
|
+
model: string;
|
|
35
|
+
receivedAt: string;
|
|
36
|
+
runtimeId: string;
|
|
37
|
+
decision: RequestGateDecision;
|
|
38
|
+
route: SafeRouteState;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type AgentGatewayDiagnostic = {
|
|
42
|
+
kind: 'agent-request-gateway';
|
|
43
|
+
requestId: string;
|
|
44
|
+
requestType: 'agent';
|
|
45
|
+
source: 'cursor-agent-exec' | 'manual-check' | 'unknown';
|
|
46
|
+
model: string;
|
|
47
|
+
receivedAt: string;
|
|
48
|
+
runtimeId: string;
|
|
49
|
+
decision: AgentGatewayDecision;
|
|
50
|
+
forward: SafeGatewayForwardState;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
type DiagnosticEntry = AgentCanaryDiagnostic | AgentRequestCheckDiagnostic | AgentGatewayDiagnostic;
|
|
54
|
+
|
|
55
|
+
const BLOCKED_GATE_REASONS = new Set<RequestGateDecision['reason']>([
|
|
56
|
+
'logged-out',
|
|
57
|
+
'mode-inactive',
|
|
58
|
+
'mode-released',
|
|
59
|
+
'device-inactive',
|
|
60
|
+
'invalid-session',
|
|
61
|
+
]);
|
|
62
|
+
const RELEASE_REASONS = new Set<NonNullable<Extract<RequestGateDecision, { state: 'blocked' }>['releaseReason']>>([
|
|
63
|
+
'product-missing',
|
|
64
|
+
'product-unavailable',
|
|
65
|
+
'insufficient-credits',
|
|
66
|
+
'invalid-token',
|
|
67
|
+
'device-inactive',
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
function resolveDiagnosticsFile(diagnosticsFile = DEFAULT_DIAGNOSTICS_FILE) {
|
|
71
|
+
if (diagnosticsFile.startsWith('~/')) {
|
|
72
|
+
return join(homedir(), diagnosticsFile.slice(2));
|
|
73
|
+
}
|
|
74
|
+
return diagnosticsFile;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function sanitizeCanaryForDiagnostics(input: AgentCanary): AgentCanaryDiagnostic {
|
|
78
|
+
return {
|
|
79
|
+
requestId: input.requestId,
|
|
80
|
+
requestType: 'agent',
|
|
81
|
+
source: 'cursor-agent-exec',
|
|
82
|
+
model: input.model,
|
|
83
|
+
receivedAt: input.receivedAt,
|
|
84
|
+
runtimeId: input.runtimeId,
|
|
85
|
+
gate: sanitizeGateForDiagnostics(input.gate),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sanitizeRequestCheckForDiagnostics(input: AgentRequestCheck): AgentRequestCheckDiagnostic {
|
|
90
|
+
return {
|
|
91
|
+
kind: 'agent-request-check',
|
|
92
|
+
requestId: input.requestId,
|
|
93
|
+
requestType: 'agent',
|
|
94
|
+
source: input.source === 'cursor-agent-exec' ? 'cursor-agent-exec' : 'manual-check',
|
|
95
|
+
model: input.model,
|
|
96
|
+
receivedAt: input.receivedAt,
|
|
97
|
+
runtimeId: input.runtimeId,
|
|
98
|
+
decision: sanitizeGateForDiagnostics(input.decision),
|
|
99
|
+
route: sanitizeRouteForDiagnostics(input.route),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function sanitizeGatewayForDiagnostics(input: AgentGateway): AgentGatewayDiagnostic {
|
|
104
|
+
return {
|
|
105
|
+
kind: 'agent-request-gateway',
|
|
106
|
+
requestId: input.requestId,
|
|
107
|
+
requestType: 'agent',
|
|
108
|
+
source: input.source,
|
|
109
|
+
model: input.model,
|
|
110
|
+
receivedAt: input.receivedAt,
|
|
111
|
+
runtimeId: input.runtimeId,
|
|
112
|
+
decision: sanitizeGatewayDecisionForDiagnostics(input.decision),
|
|
113
|
+
forward: sanitizeGatewayForward(input.forward),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function sanitizeGatewayDecisionForDiagnostics(decision: AgentGatewayDecision): AgentGatewayDecision {
|
|
118
|
+
return sanitizeHistoricalGatewayDecision(decision);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function sanitizeDiagnosticEntry(input: unknown): DiagnosticEntry | null {
|
|
122
|
+
if (!isRecord(input)) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (input.kind === 'agent-request-gateway') {
|
|
127
|
+
const decision = sanitizeHistoricalGatewayDecision(input.decision);
|
|
128
|
+
const forward = sanitizeGatewayForward(input.forward);
|
|
129
|
+
if (
|
|
130
|
+
typeof input.requestId !== 'string' ||
|
|
131
|
+
input.requestType !== 'agent' ||
|
|
132
|
+
(input.source !== 'cursor-agent-exec' && input.source !== 'manual-check' && input.source !== 'unknown') ||
|
|
133
|
+
typeof input.model !== 'string' ||
|
|
134
|
+
typeof input.receivedAt !== 'string' ||
|
|
135
|
+
typeof input.runtimeId !== 'string'
|
|
136
|
+
) {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
kind: 'agent-request-gateway',
|
|
142
|
+
requestId: input.requestId,
|
|
143
|
+
requestType: 'agent',
|
|
144
|
+
source: input.source,
|
|
145
|
+
model: input.model,
|
|
146
|
+
receivedAt: input.receivedAt,
|
|
147
|
+
runtimeId: input.runtimeId,
|
|
148
|
+
decision,
|
|
149
|
+
forward,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (input.kind === 'agent-request-check') {
|
|
154
|
+
const decision = sanitizeHistoricalGate(input.decision);
|
|
155
|
+
const route = sanitizeHistoricalRoute(input.route);
|
|
156
|
+
if (
|
|
157
|
+
typeof input.requestId !== 'string' ||
|
|
158
|
+
input.requestType !== 'agent' ||
|
|
159
|
+
(input.source !== 'cursor-agent-exec' && input.source !== 'manual-check') ||
|
|
160
|
+
typeof input.model !== 'string' ||
|
|
161
|
+
typeof input.receivedAt !== 'string' ||
|
|
162
|
+
typeof input.runtimeId !== 'string' ||
|
|
163
|
+
decision === null
|
|
164
|
+
) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
kind: 'agent-request-check',
|
|
170
|
+
requestId: input.requestId,
|
|
171
|
+
requestType: 'agent',
|
|
172
|
+
source: input.source,
|
|
173
|
+
model: input.model,
|
|
174
|
+
receivedAt: input.receivedAt,
|
|
175
|
+
runtimeId: input.runtimeId,
|
|
176
|
+
decision,
|
|
177
|
+
route,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const gate = sanitizeHistoricalGate(input.gate);
|
|
182
|
+
if (
|
|
183
|
+
typeof input.requestId !== 'string' ||
|
|
184
|
+
input.requestType !== 'agent' ||
|
|
185
|
+
input.source !== 'cursor-agent-exec' ||
|
|
186
|
+
typeof input.model !== 'string' ||
|
|
187
|
+
typeof input.receivedAt !== 'string' ||
|
|
188
|
+
typeof input.runtimeId !== 'string' ||
|
|
189
|
+
gate === null
|
|
190
|
+
) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
requestId: input.requestId,
|
|
196
|
+
requestType: 'agent',
|
|
197
|
+
source: 'cursor-agent-exec',
|
|
198
|
+
model: input.model,
|
|
199
|
+
receivedAt: input.receivedAt,
|
|
200
|
+
runtimeId: input.runtimeId,
|
|
201
|
+
gate,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function sanitizeGateForDiagnostics(gate: RequestGateDecision): RequestGateDecision {
|
|
206
|
+
if (gate.state === 'allowed') {
|
|
207
|
+
return {
|
|
208
|
+
state: 'allowed',
|
|
209
|
+
productId: gate.productId,
|
|
210
|
+
modeStartedAt: gate.modeStartedAt,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
state: 'blocked',
|
|
216
|
+
reason: gate.reason,
|
|
217
|
+
...(gate.releaseReason !== undefined ? { releaseReason: gate.releaseReason } : {}),
|
|
218
|
+
...(gate.releasedAt !== undefined ? { releasedAt: gate.releasedAt } : {}),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function sanitizeRouteForDiagnostics(route: SafeRouteState): SafeRouteState {
|
|
223
|
+
if (route.state === 'ready' || route.state === 'expired') {
|
|
224
|
+
return { state: route.state, expiresAt: route.expiresAt };
|
|
225
|
+
}
|
|
226
|
+
return { state: 'missing' };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function sanitizeHistoricalRoute(input: unknown): SafeRouteState {
|
|
230
|
+
if (!isRecord(input)) {
|
|
231
|
+
return { state: 'missing' };
|
|
232
|
+
}
|
|
233
|
+
if ((input.state === 'ready' || input.state === 'expired') && typeof input.expiresAt === 'string') {
|
|
234
|
+
return { state: input.state, expiresAt: input.expiresAt };
|
|
235
|
+
}
|
|
236
|
+
return { state: 'missing' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function sanitizeHistoricalGate(input: unknown): RequestGateDecision | null {
|
|
240
|
+
if (!isRecord(input)) {
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (input.state === 'allowed') {
|
|
245
|
+
if (typeof input.productId !== 'string' || typeof input.modeStartedAt !== 'string') {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
state: 'allowed',
|
|
251
|
+
productId: input.productId,
|
|
252
|
+
modeStartedAt: input.modeStartedAt,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (input.state === 'blocked') {
|
|
257
|
+
if (typeof input.reason !== 'string' || !BLOCKED_GATE_REASONS.has(input.reason)) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
state: 'blocked',
|
|
263
|
+
reason: input.reason,
|
|
264
|
+
...(typeof input.releaseReason === 'string' && RELEASE_REASONS.has(input.releaseReason)
|
|
265
|
+
? { releaseReason: input.releaseReason }
|
|
266
|
+
: {}),
|
|
267
|
+
...(typeof input.releasedAt === 'string' ? { releasedAt: input.releasedAt } : {}),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function sanitizeHistoricalGatewayDecision(input: unknown): AgentGatewayDecision {
|
|
275
|
+
if (!isRecord(input)) {
|
|
276
|
+
return { state: 'unknown' };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (input.state === 'accepted') {
|
|
280
|
+
const route = sanitizeHistoricalRoute(input.route);
|
|
281
|
+
if (typeof input.productId === 'string' && typeof input.modeStartedAt === 'string' && route.state === 'ready') {
|
|
282
|
+
return { state: 'accepted', productId: input.productId, modeStartedAt: input.modeStartedAt, route };
|
|
283
|
+
}
|
|
284
|
+
return { state: 'unknown' };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (input.state === 'route-missing') {
|
|
288
|
+
if (typeof input.productId === 'string' && typeof input.modeStartedAt === 'string') {
|
|
289
|
+
return {
|
|
290
|
+
state: 'route-missing',
|
|
291
|
+
productId: input.productId,
|
|
292
|
+
modeStartedAt: input.modeStartedAt,
|
|
293
|
+
route: { state: 'missing' },
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return { state: 'unknown' };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (input.state === 'route-expired') {
|
|
300
|
+
const route = sanitizeHistoricalRoute(input.route);
|
|
301
|
+
if (typeof input.productId === 'string' && typeof input.modeStartedAt === 'string' && route.state === 'expired') {
|
|
302
|
+
return { state: 'route-expired', productId: input.productId, modeStartedAt: input.modeStartedAt, route };
|
|
303
|
+
}
|
|
304
|
+
return { state: 'unknown' };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (input.state === 'blocked') {
|
|
308
|
+
const gate = sanitizeHistoricalGate(input);
|
|
309
|
+
if (gate?.state === 'blocked') {
|
|
310
|
+
return { ...gate, route: { state: 'missing' } };
|
|
311
|
+
}
|
|
312
|
+
return { state: 'unknown' };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return { state: 'unknown' };
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function isRecord(input: unknown): input is Record<string, unknown> {
|
|
319
|
+
return typeof input === 'object' && input !== null && !Array.isArray(input);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function readDiagnosticEntries(diagnosticsFile: string): Promise<DiagnosticEntry[]> {
|
|
323
|
+
try {
|
|
324
|
+
const content = await readFile(diagnosticsFile, 'utf8');
|
|
325
|
+
return content
|
|
326
|
+
.split('\n')
|
|
327
|
+
.map((line) => line.trim())
|
|
328
|
+
.filter(Boolean)
|
|
329
|
+
.flatMap((line) => {
|
|
330
|
+
try {
|
|
331
|
+
const entry = sanitizeDiagnosticEntry(JSON.parse(line) as unknown);
|
|
332
|
+
return entry === null ? [] : [entry];
|
|
333
|
+
} catch {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
} catch (error) {
|
|
338
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
339
|
+
return [];
|
|
340
|
+
}
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
export async function appendAgentCanaryDiagnostic(
|
|
346
|
+
canary: AgentCanary,
|
|
347
|
+
options: DiagnosticOptions = {},
|
|
348
|
+
) {
|
|
349
|
+
const diagnosticsFile = resolveDiagnosticsFile(options.diagnosticsFile);
|
|
350
|
+
const maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_DIAGNOSTICS);
|
|
351
|
+
const entries = await readDiagnosticEntries(diagnosticsFile);
|
|
352
|
+
const nextEntries = [...entries, sanitizeCanaryForDiagnostics(canary)].slice(-maxEntries);
|
|
353
|
+
const content = `${nextEntries.map((entry) => JSON.stringify(entry)).join('\n')}\n`;
|
|
354
|
+
|
|
355
|
+
await mkdir(dirname(diagnosticsFile), { recursive: true });
|
|
356
|
+
await writeFile(diagnosticsFile, content, 'utf8');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export async function appendAgentRequestCheckDiagnostic(
|
|
360
|
+
check: AgentRequestCheck,
|
|
361
|
+
options: DiagnosticOptions = {},
|
|
362
|
+
) {
|
|
363
|
+
const diagnosticsFile = resolveDiagnosticsFile(options.diagnosticsFile);
|
|
364
|
+
const maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_DIAGNOSTICS);
|
|
365
|
+
const entries = await readDiagnosticEntries(diagnosticsFile);
|
|
366
|
+
const nextEntries = [...entries, sanitizeRequestCheckForDiagnostics(check)].slice(-maxEntries);
|
|
367
|
+
const content = `${nextEntries.map((entry) => JSON.stringify(entry)).join('\n')}\n`;
|
|
368
|
+
|
|
369
|
+
await mkdir(dirname(diagnosticsFile), { recursive: true });
|
|
370
|
+
await writeFile(diagnosticsFile, content, 'utf8');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export async function appendAgentGatewayDiagnostic(
|
|
374
|
+
gateway: AgentGateway,
|
|
375
|
+
options: DiagnosticOptions = {},
|
|
376
|
+
) {
|
|
377
|
+
const diagnosticsFile = resolveDiagnosticsFile(options.diagnosticsFile);
|
|
378
|
+
const maxEntries = Math.max(1, options.maxEntries ?? DEFAULT_MAX_DIAGNOSTICS);
|
|
379
|
+
const entries = await readDiagnosticEntries(diagnosticsFile);
|
|
380
|
+
const nextEntries = [...entries, sanitizeGatewayForDiagnostics(gateway)].slice(-maxEntries);
|
|
381
|
+
const content = `${nextEntries.map((entry) => JSON.stringify(entry)).join('\n')}\n`;
|
|
382
|
+
|
|
383
|
+
await mkdir(dirname(diagnosticsFile), { recursive: true });
|
|
384
|
+
await writeFile(diagnosticsFile, content, 'utf8');
|
|
385
|
+
}
|