@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,81 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { SafeRouteState } from './platformSession';
|
|
3
|
+
import type { RequestGateDecision } from './requestGate';
|
|
4
|
+
|
|
5
|
+
export type AgentRequestCheckInput = {
|
|
6
|
+
requestId?: unknown;
|
|
7
|
+
requestType?: unknown;
|
|
8
|
+
source?: unknown;
|
|
9
|
+
model?: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type AgentRequestCheck = {
|
|
13
|
+
requestId: string;
|
|
14
|
+
requestType: 'agent';
|
|
15
|
+
source: 'cursor-agent-exec' | 'manual-check';
|
|
16
|
+
model: string;
|
|
17
|
+
receivedAt: string;
|
|
18
|
+
runtimeId: string;
|
|
19
|
+
decision: RequestGateDecision;
|
|
20
|
+
route: SafeRouteState;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type SanitizeAgentRequestCheckOptions = {
|
|
24
|
+
runtimeId: string;
|
|
25
|
+
decision: RequestGateDecision;
|
|
26
|
+
route?: SafeRouteState;
|
|
27
|
+
now?: () => string;
|
|
28
|
+
requestId?: () => string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const SAFE_MODEL_PATTERN = /^[A-Za-z0-9._:-]{1,96}$/;
|
|
32
|
+
const SECRET_PATTERN = /(api[_-]?key|authorization|bearer|cursor[_-]?auth|provider[_-]?secret|secret|token|sk-[A-Za-z0-9])/i;
|
|
33
|
+
|
|
34
|
+
function isValidRequestId(value: unknown): value is string {
|
|
35
|
+
return typeof value === 'string' && value.length > 0 && value.length <= 128;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function sanitizeSource(value: unknown): AgentRequestCheck['source'] {
|
|
39
|
+
return value === 'cursor-agent-exec' || value === 'manual-check' ? value : 'manual-check';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sanitizeModel(value: unknown): string {
|
|
43
|
+
if (typeof value !== 'string' || !SAFE_MODEL_PATTERN.test(value) || SECRET_PATTERN.test(value)) {
|
|
44
|
+
return 'unknown';
|
|
45
|
+
}
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function sanitizeAgentRequestCheck(
|
|
50
|
+
input: AgentRequestCheckInput,
|
|
51
|
+
options: SanitizeAgentRequestCheckOptions,
|
|
52
|
+
): AgentRequestCheck {
|
|
53
|
+
const requestId = isValidRequestId(input.requestId)
|
|
54
|
+
? input.requestId
|
|
55
|
+
: (options.requestId ?? randomUUID)();
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
requestId,
|
|
59
|
+
requestType: 'agent',
|
|
60
|
+
source: sanitizeSource(input.source),
|
|
61
|
+
model: sanitizeModel(input.model),
|
|
62
|
+
receivedAt: (options.now ?? (() => new Date().toISOString()))(),
|
|
63
|
+
runtimeId: options.runtimeId,
|
|
64
|
+
decision: options.decision,
|
|
65
|
+
route: options.route ?? { state: 'missing' },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function createRequestCheckStore() {
|
|
70
|
+
let latestCheck: AgentRequestCheck | null = null;
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
record(check: AgentRequestCheck): AgentRequestCheck {
|
|
74
|
+
latestCheck = check;
|
|
75
|
+
return check;
|
|
76
|
+
},
|
|
77
|
+
latest(): AgentRequestCheck | null {
|
|
78
|
+
return latestCheck;
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
readPlatformSessionSnapshot,
|
|
3
|
+
type PlatformModeReleaseReason,
|
|
4
|
+
type PlatformSessionOptions,
|
|
5
|
+
type SafeRouteState,
|
|
6
|
+
} from './platformSession';
|
|
7
|
+
|
|
8
|
+
export type RequestGateDecision =
|
|
9
|
+
| { state: 'allowed'; productId: string; modeStartedAt: string }
|
|
10
|
+
| {
|
|
11
|
+
state: 'blocked';
|
|
12
|
+
reason: 'logged-out' | 'mode-inactive' | 'mode-released' | 'device-inactive' | 'invalid-session';
|
|
13
|
+
releaseReason?: PlatformModeReleaseReason;
|
|
14
|
+
releasedAt?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export const INVALID_SESSION_GATE: RequestGateDecision = { state: 'blocked', reason: 'invalid-session' };
|
|
18
|
+
|
|
19
|
+
function invalidSessionGate(): RequestGateDecision {
|
|
20
|
+
return { ...INVALID_SESSION_GATE };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function hasNonEmptyString(value: unknown): value is string {
|
|
24
|
+
return typeof value === 'string' && value.length > 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function evaluateRequestGate(
|
|
28
|
+
options: PlatformSessionOptions = {},
|
|
29
|
+
): Promise<RequestGateDecision> {
|
|
30
|
+
const snapshot = await readPlatformSessionSnapshot(options);
|
|
31
|
+
|
|
32
|
+
if (snapshot.state === 'missing') {
|
|
33
|
+
return { state: 'blocked', reason: 'logged-out' };
|
|
34
|
+
}
|
|
35
|
+
if (snapshot.state === 'invalid') {
|
|
36
|
+
return invalidSessionGate();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { session } = snapshot;
|
|
40
|
+
if (session.device.status !== 'active') {
|
|
41
|
+
return { state: 'blocked', reason: 'device-inactive' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
session.platformMode !== 'active' &&
|
|
46
|
+
session.lastModeReleaseReason &&
|
|
47
|
+
session.lastModeReleasedAt
|
|
48
|
+
) {
|
|
49
|
+
return {
|
|
50
|
+
state: 'blocked',
|
|
51
|
+
reason: 'mode-released',
|
|
52
|
+
releaseReason: session.lastModeReleaseReason,
|
|
53
|
+
releasedAt: session.lastModeReleasedAt,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
session.platformMode !== 'active' ||
|
|
59
|
+
!hasNonEmptyString(session.activeProductId) ||
|
|
60
|
+
!hasNonEmptyString(session.platformModeStartedAt)
|
|
61
|
+
) {
|
|
62
|
+
return { state: 'blocked', reason: 'mode-inactive' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
state: 'allowed',
|
|
67
|
+
productId: session.activeProductId,
|
|
68
|
+
modeStartedAt: session.platformModeStartedAt,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type RouteStateOptions = PlatformSessionOptions & {
|
|
73
|
+
now?: () => string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export async function evaluateRouteState(
|
|
77
|
+
options: RouteStateOptions = {},
|
|
78
|
+
): Promise<SafeRouteState> {
|
|
79
|
+
const snapshot = await readPlatformSessionSnapshot(options);
|
|
80
|
+
if (snapshot.state !== 'valid') {
|
|
81
|
+
return { state: 'missing' };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const { routeToken } = snapshot.session;
|
|
85
|
+
if (!routeToken) {
|
|
86
|
+
return { state: 'missing' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const expiresAt = Date.parse(routeToken.expiresAt);
|
|
90
|
+
const now = Date.parse((options.now ?? (() => new Date().toISOString()))());
|
|
91
|
+
if (Number.isNaN(expiresAt) || Number.isNaN(now)) {
|
|
92
|
+
return { state: 'missing' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (expiresAt <= now) {
|
|
96
|
+
return { state: 'expired', expiresAt: routeToken.expiresAt };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { state: 'ready', expiresAt: routeToken.expiresAt };
|
|
100
|
+
}
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import type { PlatformModeReleaseReason, SafeRouteState } from './platformSession';
|
|
3
|
+
import type { RequestGateDecision } from './requestGate';
|
|
4
|
+
|
|
5
|
+
export type AgentGatewayInput = {
|
|
6
|
+
requestId?: unknown;
|
|
7
|
+
requestType?: unknown;
|
|
8
|
+
source?: unknown;
|
|
9
|
+
model?: unknown;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type AgentGatewayDecision =
|
|
13
|
+
| {
|
|
14
|
+
state: 'accepted';
|
|
15
|
+
productId: string;
|
|
16
|
+
modeStartedAt: string;
|
|
17
|
+
route: Extract<SafeRouteState, { state: 'ready' }>;
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
state: 'route-missing';
|
|
21
|
+
productId: string;
|
|
22
|
+
modeStartedAt: string;
|
|
23
|
+
route: Extract<SafeRouteState, { state: 'missing' }>;
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
state: 'route-expired';
|
|
27
|
+
productId: string;
|
|
28
|
+
modeStartedAt: string;
|
|
29
|
+
route: Extract<SafeRouteState, { state: 'expired' }>;
|
|
30
|
+
}
|
|
31
|
+
| {
|
|
32
|
+
state: 'blocked';
|
|
33
|
+
reason: 'logged-out' | 'mode-inactive' | 'mode-released' | 'device-inactive' | 'invalid-session';
|
|
34
|
+
releaseReason?: PlatformModeReleaseReason;
|
|
35
|
+
releasedAt?: string;
|
|
36
|
+
route: Extract<SafeRouteState, { state: 'missing' }>;
|
|
37
|
+
}
|
|
38
|
+
| { state: 'unknown' };
|
|
39
|
+
|
|
40
|
+
export type GatewayForwardRejectReason =
|
|
41
|
+
| 'route-token-invalid'
|
|
42
|
+
| 'pool-session-expired'
|
|
43
|
+
| 'product-unavailable'
|
|
44
|
+
| 'model-banned'
|
|
45
|
+
| 'provider-unavailable'
|
|
46
|
+
| 'rate-limited'
|
|
47
|
+
| 'insufficient-credits'
|
|
48
|
+
| 'manual-review-required';
|
|
49
|
+
|
|
50
|
+
export type GatewayForwardResult =
|
|
51
|
+
| {
|
|
52
|
+
state: 'forwarded';
|
|
53
|
+
upstreamRequestId: string;
|
|
54
|
+
acceptedAt: string;
|
|
55
|
+
routeExpiresAt: string;
|
|
56
|
+
usageRequestId?: string;
|
|
57
|
+
content?: string;
|
|
58
|
+
openAiResponse?: Record<string, unknown>;
|
|
59
|
+
openAiStreamEvents?: Record<string, unknown>[];
|
|
60
|
+
}
|
|
61
|
+
| {
|
|
62
|
+
state: 'rejected';
|
|
63
|
+
reason: GatewayForwardRejectReason;
|
|
64
|
+
retryAfterMs?: number;
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
state: 'not-configured';
|
|
68
|
+
}
|
|
69
|
+
| {
|
|
70
|
+
state: 'timeout';
|
|
71
|
+
}
|
|
72
|
+
| {
|
|
73
|
+
state: 'network-error';
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export type SafeGatewayForwardState =
|
|
77
|
+
| {
|
|
78
|
+
state: 'skipped';
|
|
79
|
+
reason: 'not-accepted';
|
|
80
|
+
}
|
|
81
|
+
| {
|
|
82
|
+
state: 'forwarded';
|
|
83
|
+
upstreamRequestId: string;
|
|
84
|
+
acceptedAt: string;
|
|
85
|
+
content?: string;
|
|
86
|
+
}
|
|
87
|
+
| {
|
|
88
|
+
state: 'rejected';
|
|
89
|
+
reason: GatewayForwardRejectReason;
|
|
90
|
+
retryAfterMs?: number;
|
|
91
|
+
}
|
|
92
|
+
| {
|
|
93
|
+
state: 'not-configured';
|
|
94
|
+
}
|
|
95
|
+
| {
|
|
96
|
+
state: 'timeout';
|
|
97
|
+
}
|
|
98
|
+
| {
|
|
99
|
+
state: 'network-error';
|
|
100
|
+
}
|
|
101
|
+
| {
|
|
102
|
+
state: 'unknown';
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export type AgentGateway = {
|
|
106
|
+
requestId: string;
|
|
107
|
+
requestType: 'agent';
|
|
108
|
+
source: 'cursor-agent-exec' | 'manual-check' | 'unknown';
|
|
109
|
+
model: string;
|
|
110
|
+
receivedAt: string;
|
|
111
|
+
runtimeId: string;
|
|
112
|
+
decision: AgentGatewayDecision;
|
|
113
|
+
forward: SafeGatewayForwardState;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export type GatewayForwardRequest = {
|
|
117
|
+
gateway: AgentGateway;
|
|
118
|
+
routeToken: string;
|
|
119
|
+
poolSessionId?: string;
|
|
120
|
+
productId: string;
|
|
121
|
+
model: string;
|
|
122
|
+
requestId: string;
|
|
123
|
+
settlementMode?: 'provider_forwarded' | 'client_response';
|
|
124
|
+
openAiRequest?: Record<string, unknown>;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export type GatewayForwarder = (request: GatewayForwardRequest) => Promise<GatewayForwardResult>;
|
|
128
|
+
|
|
129
|
+
export type GatewayHttpForwarderOptions = {
|
|
130
|
+
apiBaseUrl?: string;
|
|
131
|
+
deviceToken?: string;
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export type GatewayCompleteOptions = GatewayHttpForwarderOptions & {
|
|
135
|
+
requestId: string;
|
|
136
|
+
routeToken: string;
|
|
137
|
+
poolSessionId: string;
|
|
138
|
+
productId: string;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export type GatewayCompleteResult =
|
|
142
|
+
| {
|
|
143
|
+
state: 'charged' | 'pending_bill' | 'already_settled' | 'failed';
|
|
144
|
+
usageRequestId: string;
|
|
145
|
+
chargeStatus: string;
|
|
146
|
+
chargeCredits: number;
|
|
147
|
+
}
|
|
148
|
+
| {
|
|
149
|
+
state: 'not-configured' | 'network-error';
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
type SanitizeAgentGatewayOptions = {
|
|
153
|
+
runtimeId: string;
|
|
154
|
+
decision: AgentGatewayDecision;
|
|
155
|
+
now?: () => string;
|
|
156
|
+
requestId?: () => string;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const SAFE_MODEL_PATTERN = /^[A-Za-z0-9._:-]{1,96}$/;
|
|
160
|
+
const SAFE_REQUEST_ID_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
161
|
+
const SAFE_FORWARD_TOKEN_PATTERN = /^[A-Za-z0-9._:-]{1,128}$/;
|
|
162
|
+
const SECRET_PATTERN = /(api[_-]?key|authorization|bearer|cursor[_-]?auth|provider[_-]?secret|secret|token|sk-[A-Za-z0-9])/i;
|
|
163
|
+
const MAX_RETRY_AFTER_MS = 86_400_000;
|
|
164
|
+
const MAX_FORWARD_CONTENT_LENGTH = 8000;
|
|
165
|
+
const FORWARD_REJECT_REASONS = new Set<GatewayForwardRejectReason>([
|
|
166
|
+
'route-token-invalid',
|
|
167
|
+
'pool-session-expired',
|
|
168
|
+
'product-unavailable',
|
|
169
|
+
'model-banned',
|
|
170
|
+
'provider-unavailable',
|
|
171
|
+
'rate-limited',
|
|
172
|
+
'insufficient-credits',
|
|
173
|
+
'manual-review-required',
|
|
174
|
+
]);
|
|
175
|
+
|
|
176
|
+
function isValidRequestId(value: unknown): value is string {
|
|
177
|
+
return typeof value === 'string' && SAFE_REQUEST_ID_PATTERN.test(value) && !SECRET_PATTERN.test(value);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function sanitizeSource(value: unknown): AgentGateway['source'] {
|
|
181
|
+
return value === 'cursor-agent-exec' || value === 'manual-check' ? value : 'unknown';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function sanitizeModel(value: unknown): string {
|
|
185
|
+
if (typeof value !== 'string' || !SAFE_MODEL_PATTERN.test(value) || SECRET_PATTERN.test(value)) {
|
|
186
|
+
return 'unknown';
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isSafeForwardToken(value: unknown): value is string {
|
|
192
|
+
return typeof value === 'string' && SAFE_FORWARD_TOKEN_PATTERN.test(value) && !SECRET_PATTERN.test(value);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isSafeIsoTimestamp(value: unknown): value is string {
|
|
196
|
+
return isSafeForwardToken(value) && !Number.isNaN(Date.parse(value));
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function safeForwardContent(value: unknown): string | undefined {
|
|
200
|
+
if (typeof value !== 'string') {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
const content = value.trim();
|
|
204
|
+
if (!content || SECRET_PATTERN.test(content)) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
return content.slice(0, MAX_FORWARD_CONTENT_LENGTH);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function evaluateGatewayDecision(
|
|
211
|
+
gate: RequestGateDecision,
|
|
212
|
+
route: SafeRouteState,
|
|
213
|
+
): AgentGatewayDecision {
|
|
214
|
+
if (gate.state === 'blocked') {
|
|
215
|
+
return {
|
|
216
|
+
...gate,
|
|
217
|
+
route: { state: 'missing' },
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (route.state === 'ready') {
|
|
222
|
+
return {
|
|
223
|
+
state: 'accepted',
|
|
224
|
+
productId: gate.productId,
|
|
225
|
+
modeStartedAt: gate.modeStartedAt,
|
|
226
|
+
route,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (route.state === 'expired') {
|
|
231
|
+
return {
|
|
232
|
+
state: 'route-expired',
|
|
233
|
+
productId: gate.productId,
|
|
234
|
+
modeStartedAt: gate.modeStartedAt,
|
|
235
|
+
route,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
state: 'route-missing',
|
|
241
|
+
productId: gate.productId,
|
|
242
|
+
modeStartedAt: gate.modeStartedAt,
|
|
243
|
+
route,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function sanitizeAgentGateway(
|
|
248
|
+
input: AgentGatewayInput,
|
|
249
|
+
options: SanitizeAgentGatewayOptions,
|
|
250
|
+
): AgentGateway {
|
|
251
|
+
const requestId = isValidRequestId(input.requestId)
|
|
252
|
+
? input.requestId
|
|
253
|
+
: (options.requestId ?? randomUUID)();
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
requestId,
|
|
257
|
+
requestType: 'agent',
|
|
258
|
+
source: sanitizeSource(input.source),
|
|
259
|
+
model: sanitizeModel(input.model),
|
|
260
|
+
receivedAt: (options.now ?? (() => new Date().toISOString()))(),
|
|
261
|
+
runtimeId: options.runtimeId,
|
|
262
|
+
decision: options.decision,
|
|
263
|
+
forward: { state: 'unknown' },
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
export function sanitizeGatewayForward(input: unknown): SafeGatewayForwardState {
|
|
268
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
269
|
+
return { state: 'unknown' };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const forward = input as Record<string, unknown>;
|
|
273
|
+
if (forward.state === 'skipped') {
|
|
274
|
+
return forward.reason === 'not-accepted'
|
|
275
|
+
? { state: 'skipped', reason: 'not-accepted' }
|
|
276
|
+
: { state: 'unknown' };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (forward.state === 'forwarded') {
|
|
280
|
+
if (!isSafeForwardToken(forward.upstreamRequestId) || !isSafeIsoTimestamp(forward.acceptedAt)) {
|
|
281
|
+
return { state: 'unknown' };
|
|
282
|
+
}
|
|
283
|
+
const content = safeForwardContent(forward.content);
|
|
284
|
+
return {
|
|
285
|
+
state: 'forwarded',
|
|
286
|
+
upstreamRequestId: forward.upstreamRequestId,
|
|
287
|
+
acceptedAt: forward.acceptedAt,
|
|
288
|
+
...(content ? { content } : {}),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (forward.state === 'rejected') {
|
|
293
|
+
if (typeof forward.reason !== 'string' || !FORWARD_REJECT_REASONS.has(forward.reason as GatewayForwardRejectReason)) {
|
|
294
|
+
return { state: 'unknown' };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const retryAfterMs = forward.retryAfterMs;
|
|
298
|
+
return {
|
|
299
|
+
state: 'rejected',
|
|
300
|
+
reason: forward.reason as GatewayForwardRejectReason,
|
|
301
|
+
...(Number.isInteger(retryAfterMs) && retryAfterMs >= 0 && retryAfterMs <= MAX_RETRY_AFTER_MS
|
|
302
|
+
? { retryAfterMs }
|
|
303
|
+
: {}),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (forward.state === 'not-configured' || forward.state === 'timeout' || forward.state === 'network-error') {
|
|
308
|
+
return { state: forward.state };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { state: 'unknown' };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export async function resolveGatewayForward(options: GatewayForwardRequest & {
|
|
315
|
+
forwarder?: GatewayForwarder;
|
|
316
|
+
}): Promise<SafeGatewayForwardState> {
|
|
317
|
+
const { safe } = await resolveGatewayForwardResult(options);
|
|
318
|
+
return safe;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export async function resolveGatewayForwardResult(options: GatewayForwardRequest & {
|
|
322
|
+
forwarder?: GatewayForwarder;
|
|
323
|
+
}): Promise<{ result: GatewayForwardResult; safe: SafeGatewayForwardState }> {
|
|
324
|
+
if (options.gateway.decision.state !== 'accepted') {
|
|
325
|
+
return {
|
|
326
|
+
result: { state: 'not-configured' },
|
|
327
|
+
safe: { state: 'skipped', reason: 'not-accepted' },
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (!options.forwarder) {
|
|
332
|
+
return {
|
|
333
|
+
result: { state: 'not-configured' },
|
|
334
|
+
safe: { state: 'not-configured' },
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
try {
|
|
339
|
+
const result = await options.forwarder({
|
|
340
|
+
gateway: options.gateway,
|
|
341
|
+
routeToken: options.routeToken,
|
|
342
|
+
poolSessionId: options.poolSessionId,
|
|
343
|
+
productId: options.productId,
|
|
344
|
+
model: options.model,
|
|
345
|
+
requestId: options.requestId,
|
|
346
|
+
settlementMode: options.settlementMode,
|
|
347
|
+
...(options.openAiRequest ? { openAiRequest: options.openAiRequest } : {}),
|
|
348
|
+
});
|
|
349
|
+
return {
|
|
350
|
+
result,
|
|
351
|
+
safe: sanitizeGatewayForward(result),
|
|
352
|
+
};
|
|
353
|
+
} catch {
|
|
354
|
+
return {
|
|
355
|
+
result: { state: 'network-error' },
|
|
356
|
+
safe: { state: 'network-error' },
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function cleanApiBaseUrl(apiBaseUrl: string) {
|
|
362
|
+
return apiBaseUrl.replace(/\/+$/, '');
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export function createGatewayHttpForwarder(options: GatewayHttpForwarderOptions): GatewayForwarder {
|
|
366
|
+
return async (request) => {
|
|
367
|
+
if (!options.apiBaseUrl || !options.deviceToken) {
|
|
368
|
+
return { state: 'not-configured' };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const response = await fetch(`${cleanApiBaseUrl(options.apiBaseUrl)}/api/client/gateway/agent`, {
|
|
372
|
+
method: 'POST',
|
|
373
|
+
headers: {
|
|
374
|
+
'content-type': 'application/json',
|
|
375
|
+
authorization: `Bearer ${options.deviceToken}`,
|
|
376
|
+
},
|
|
377
|
+
body: JSON.stringify({
|
|
378
|
+
gateway: request.gateway,
|
|
379
|
+
routeToken: request.routeToken,
|
|
380
|
+
...(request.poolSessionId ? { poolSessionId: request.poolSessionId } : {}),
|
|
381
|
+
productId: request.productId,
|
|
382
|
+
model: request.model,
|
|
383
|
+
requestId: request.requestId,
|
|
384
|
+
settlementMode: request.settlementMode ?? 'provider_forwarded',
|
|
385
|
+
...(request.openAiRequest ? { openAiRequest: request.openAiRequest } : {}),
|
|
386
|
+
}),
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
if (!response.ok) {
|
|
390
|
+
return { state: 'network-error' };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return (await response.json()) as GatewayForwardResult;
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export async function completeGatewayForward(options: GatewayCompleteOptions): Promise<GatewayCompleteResult> {
|
|
398
|
+
if (!options.apiBaseUrl || !options.deviceToken) {
|
|
399
|
+
return { state: 'not-configured' };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
try {
|
|
403
|
+
const response = await fetch(
|
|
404
|
+
`${cleanApiBaseUrl(options.apiBaseUrl)}/api/client/gateway/agent/${encodeURIComponent(options.requestId)}/complete`,
|
|
405
|
+
{
|
|
406
|
+
method: 'POST',
|
|
407
|
+
headers: {
|
|
408
|
+
'content-type': 'application/json',
|
|
409
|
+
authorization: `Bearer ${options.deviceToken}`,
|
|
410
|
+
},
|
|
411
|
+
body: JSON.stringify({
|
|
412
|
+
routeToken: options.routeToken,
|
|
413
|
+
poolSessionId: options.poolSessionId,
|
|
414
|
+
productId: options.productId,
|
|
415
|
+
}),
|
|
416
|
+
},
|
|
417
|
+
);
|
|
418
|
+
|
|
419
|
+
if (!response.ok) {
|
|
420
|
+
return { state: 'network-error' };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return (await response.json()) as GatewayCompleteResult;
|
|
424
|
+
} catch {
|
|
425
|
+
return { state: 'network-error' };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
export function createGatewayStore() {
|
|
430
|
+
let latestGateway: AgentGateway | null = null;
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
record(gateway: AgentGateway): AgentGateway {
|
|
434
|
+
latestGateway = gateway;
|
|
435
|
+
return gateway;
|
|
436
|
+
},
|
|
437
|
+
latest(): AgentGateway | null {
|
|
438
|
+
return latestGateway;
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { DEFAULT_RUNTIME_FILE } from '@cursor-pool/shared/runtime';
|
|
6
|
+
|
|
7
|
+
export type RuntimeInfo = {
|
|
8
|
+
host: '127.0.0.1';
|
|
9
|
+
port: number;
|
|
10
|
+
runtimeId: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type RuntimeOptions = {
|
|
14
|
+
runtimeFile?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function createRuntimeId() {
|
|
18
|
+
return randomUUID();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function resolveRuntimeFile(runtimeFile = DEFAULT_RUNTIME_FILE) {
|
|
22
|
+
if (runtimeFile.startsWith('~/')) {
|
|
23
|
+
return join(homedir(), runtimeFile.slice(2));
|
|
24
|
+
}
|
|
25
|
+
return runtimeFile;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function writeRuntimeInfo(runtime: RuntimeInfo, options: RuntimeOptions = {}) {
|
|
29
|
+
const runtimeFile = resolveRuntimeFile(options.runtimeFile);
|
|
30
|
+
await mkdir(dirname(runtimeFile), { recursive: true });
|
|
31
|
+
await writeFile(runtimeFile, `${JSON.stringify(runtime, null, 2)}\n`, 'utf8');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function readRuntimeInfo(options: RuntimeOptions = {}): Promise<RuntimeInfo | null> {
|
|
35
|
+
const runtimeFile = resolveRuntimeFile(options.runtimeFile);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(await readFile(runtimeFile, 'utf8')) as RuntimeInfo;
|
|
39
|
+
} catch (error) {
|
|
40
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (error instanceof SyntaxError) {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
}
|