sneakoscope 10.5.8 → 10.6.0
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/README.md +1 -1
- package/crates/sks-core/Cargo.lock +1 -1
- package/crates/sks-core/Cargo.toml +1 -1
- package/dist/config/skills-manifest.json +1 -1
- package/dist/core/agents/agent-orchestrator.js +10 -7
- package/dist/core/codex-lb/desktop-bridge/compaction-adapter.js +137 -6
- package/dist/core/codex-lb/desktop-bridge/http-forward.js +111 -39
- package/dist/core/codex-lb/desktop-bridge/responses-websocket.js +22 -14
- package/dist/core/codex-lb/desktop-bridge/security.js +41 -8
- package/dist/core/codex-lb/desktop-bridge/server.js +3 -0
- package/dist/core/fsx.js +63 -0
- package/dist/core/version.js +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
Sneakoscope Codex (`sks`) is an open-source local tool for the Codex CLI and Codex desktop app. It adds a macOS control center, bounded agent workflows, repository context, and evidence checks for SKS releases.
|
|
18
18
|
<!-- END SKS SEARCH VISIBILITY MARKETING -->
|
|
19
19
|
|
|
20
|
-
Current package: **SKS 10.
|
|
20
|
+
Current package: **SKS 10.6.0**, prepared for npm publication.
|
|
21
21
|
[Release notes](https://github.com/mandarange/Sneakoscope-Codex/releases/tag/v10.5.8)
|
|
22
22
|
· [Changelog](CHANGELOG.md). Install the latest stable release from npm.
|
|
23
23
|
|
|
@@ -663,13 +663,16 @@ export async function resolveAgentMissionPrompt(root, opts) {
|
|
|
663
663
|
readJson(path.join(dir, 'route-context.json'), null),
|
|
664
664
|
readJson(path.join(dir, 'mission.json'), null)
|
|
665
665
|
]);
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
666
|
+
const candidates = [
|
|
667
|
+
intake?.transformed_prompt,
|
|
668
|
+
routeContext?.transformed_prompt,
|
|
669
|
+
routeContext?.task,
|
|
670
|
+
routeContext?.prompt,
|
|
671
|
+
mission?.prompt,
|
|
672
|
+
direct,
|
|
673
|
+
'Native agent run'
|
|
674
|
+
];
|
|
675
|
+
return candidates.find((value) => typeof value === 'string' && value.trim().length > 0).trim();
|
|
673
676
|
}
|
|
674
677
|
function normalizeMissionHardTimeoutMs(opts = {}, route = '') {
|
|
675
678
|
const raw = Number(opts.hardTimeoutMs || opts.agentHardTimeoutMs || process.env.SKS_AGENT_HARD_TIMEOUT_MS || 0);
|
|
@@ -13,9 +13,15 @@ export const BRIDGE_COMPACTION_ENVELOPE_PREFIX = 'sks-bridge-compaction.v1.';
|
|
|
13
13
|
const COMPACTION_ITEM_TYPES = new Set(['compaction', 'compaction_summary', 'context_compaction']);
|
|
14
14
|
const MAX_SUMMARY_CHARS = 512 * 1024;
|
|
15
15
|
const MAX_RETAINED_USER_MESSAGE_CHARS = 80_000;
|
|
16
|
+
const SUMMARY_REASONING_EFFORT_CEILING = 'medium';
|
|
17
|
+
const EFFORTS_ABOVE_SUMMARY_CEILING = new Set(['high', 'xhigh', 'max', 'ultra']);
|
|
16
18
|
function isObject(value) {
|
|
17
19
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
18
20
|
}
|
|
21
|
+
export function safeUpstreamErrorId(value) {
|
|
22
|
+
const text = String(value ?? '').trim();
|
|
23
|
+
return /^[A-Za-z0-9_.:-]{1,64}$/.test(text) ? text : null;
|
|
24
|
+
}
|
|
19
25
|
export function isCompactionTriggerItem(item) {
|
|
20
26
|
return isObject(item) && item.type === 'compaction_trigger';
|
|
21
27
|
}
|
|
@@ -56,6 +62,10 @@ export function expandBridgeCompactionItems(payload) {
|
|
|
56
62
|
payload.input = input;
|
|
57
63
|
return changed;
|
|
58
64
|
}
|
|
65
|
+
function carriesToolDefinitions(body) {
|
|
66
|
+
return (Array.isArray(body.tools) && body.tools.length > 0)
|
|
67
|
+
|| (Array.isArray(body.input) && body.input.some((item) => isObject(item) && item.type === 'additional_tools'));
|
|
68
|
+
}
|
|
59
69
|
export function buildCompactionSummaryRequest(payload, upstreamModel) {
|
|
60
70
|
const { type: _eventType, ...request } = payload;
|
|
61
71
|
const history = Array.isArray(payload.input) ? payload.input.filter((item) => !isCompactionTriggerItem(item)) : [];
|
|
@@ -68,17 +78,110 @@ export function buildCompactionSummaryRequest(payload, upstreamModel) {
|
|
|
68
78
|
],
|
|
69
79
|
stream: true,
|
|
70
80
|
};
|
|
71
|
-
if (
|
|
81
|
+
if (carriesToolDefinitions(body))
|
|
72
82
|
body.tool_choice = 'none';
|
|
73
83
|
else {
|
|
74
84
|
delete body.tools;
|
|
75
85
|
delete body.tool_choice;
|
|
76
86
|
}
|
|
77
|
-
|
|
87
|
+
body.parallel_tool_calls = false;
|
|
88
|
+
if (isObject(body.reasoning) && EFFORTS_ABOVE_SUMMARY_CEILING.has(String(body.reasoning.effort))) {
|
|
89
|
+
body.reasoning = { ...body.reasoning, effort: SUMMARY_REASONING_EFFORT_CEILING };
|
|
90
|
+
}
|
|
78
91
|
delete body.previous_response_id;
|
|
79
92
|
expandBridgeCompactionItems(body);
|
|
80
93
|
return body;
|
|
81
94
|
}
|
|
95
|
+
function frameEvent(frame) {
|
|
96
|
+
try {
|
|
97
|
+
const event = JSON.parse(frame.toString('utf8'));
|
|
98
|
+
return isObject(event) ? event : null;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
export class ResponsesContinuationChain {
|
|
105
|
+
maxBytes;
|
|
106
|
+
links = [];
|
|
107
|
+
bytes = 0;
|
|
108
|
+
constructor(maxBytes) {
|
|
109
|
+
this.maxBytes = maxBytes;
|
|
110
|
+
}
|
|
111
|
+
sent(create, frame) {
|
|
112
|
+
const previous = typeof create.previous_response_id === 'string' ? create.previous_response_id : null;
|
|
113
|
+
const last = this.links?.at(-1);
|
|
114
|
+
const link = { create: frame, outputs: [], responseId: null, settled: false };
|
|
115
|
+
if (!previous) {
|
|
116
|
+
this.links = [link];
|
|
117
|
+
this.bytes = frame.length;
|
|
118
|
+
}
|
|
119
|
+
else if (this.links && last?.settled && last.responseId === previous) {
|
|
120
|
+
this.links.push(link);
|
|
121
|
+
this.track(frame.length);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
this.links = null;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
received(frame) {
|
|
128
|
+
const link = this.links?.at(-1);
|
|
129
|
+
if (!link || link.settled)
|
|
130
|
+
return;
|
|
131
|
+
if (frame.includes('"response.output_item.done"')) {
|
|
132
|
+
link.outputs.push(frame);
|
|
133
|
+
this.track(frame.length);
|
|
134
|
+
}
|
|
135
|
+
else if (frame.includes('"response.completed"')) {
|
|
136
|
+
const event = frameEvent(frame);
|
|
137
|
+
if (event?.type !== 'response.completed')
|
|
138
|
+
return;
|
|
139
|
+
link.settled = true;
|
|
140
|
+
link.responseId = isObject(event.response) && typeof event.response.id === 'string' ? event.response.id : null;
|
|
141
|
+
}
|
|
142
|
+
else if (frame.includes('"response.failed"') || frame.includes('"response.incomplete"') || frame.includes('"type":"error"')) {
|
|
143
|
+
const type = frameEvent(frame)?.type;
|
|
144
|
+
if (type === 'response.failed' || type === 'response.incomplete' || type === 'error') {
|
|
145
|
+
link.settled = true;
|
|
146
|
+
link.responseId = null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
fullInput(create) {
|
|
151
|
+
const delta = Array.isArray(create.input) ? create.input : [];
|
|
152
|
+
const previous = typeof create.previous_response_id === 'string' ? create.previous_response_id : null;
|
|
153
|
+
if (!previous)
|
|
154
|
+
return delta;
|
|
155
|
+
const last = this.links?.at(-1);
|
|
156
|
+
if (!this.links || !last?.settled || last.responseId !== previous)
|
|
157
|
+
return null;
|
|
158
|
+
const input = [];
|
|
159
|
+
for (const link of this.links) {
|
|
160
|
+
const sent = frameEvent(link.create);
|
|
161
|
+
if (!sent || !Array.isArray(sent.input))
|
|
162
|
+
return null;
|
|
163
|
+
input.push(...sent.input);
|
|
164
|
+
for (const frame of link.outputs) {
|
|
165
|
+
const event = frameEvent(frame);
|
|
166
|
+
if (event?.type === 'response.output_item.done' && isObject(event.item))
|
|
167
|
+
input.push(event.item);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return [...input, ...delta];
|
|
171
|
+
}
|
|
172
|
+
track(size) {
|
|
173
|
+
this.bytes += size;
|
|
174
|
+
if (this.bytes > this.maxBytes)
|
|
175
|
+
this.links = null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
function summaryError(error, fallback) {
|
|
179
|
+
const record = isObject(error) ? error : {};
|
|
180
|
+
return {
|
|
181
|
+
code: safeUpstreamErrorId(record.code) ?? safeUpstreamErrorId(record.type) ?? fallback,
|
|
182
|
+
param: safeUpstreamErrorId(record.param),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
82
185
|
function messageText(item) {
|
|
83
186
|
if (!isObject(item) || item.type !== 'message' || (item.role !== undefined && item.role !== 'assistant'))
|
|
84
187
|
return '';
|
|
@@ -97,10 +200,16 @@ export function parseProviderSummary(text) {
|
|
|
97
200
|
const parsed = JSON.parse(trimmed);
|
|
98
201
|
const response = isObject(parsed) && isObject(parsed.response) ? parsed.response : parsed;
|
|
99
202
|
const summary = outputText(isObject(response) ? response.output : null).trim();
|
|
100
|
-
|
|
203
|
+
const failure = isObject(response) && isObject(response.error) ? summaryError(response.error, 'response_failed') : null;
|
|
204
|
+
return {
|
|
205
|
+
summary: summary || null,
|
|
206
|
+
usage: isObject(response) && isObject(response.usage) ? response.usage : null,
|
|
207
|
+
failed: !summary,
|
|
208
|
+
error: summary ? null : failure,
|
|
209
|
+
};
|
|
101
210
|
}
|
|
102
211
|
catch {
|
|
103
|
-
return { summary: null, usage: null, failed: true };
|
|
212
|
+
return { summary: null, usage: null, failed: true, error: null };
|
|
104
213
|
}
|
|
105
214
|
}
|
|
106
215
|
const finished = [];
|
|
@@ -109,6 +218,7 @@ export function parseProviderSummary(text) {
|
|
|
109
218
|
let usage = null;
|
|
110
219
|
let completed = false;
|
|
111
220
|
let failed = false;
|
|
221
|
+
let error = null;
|
|
112
222
|
for (const block of text.split(/\r?\n\r?\n/)) {
|
|
113
223
|
const data = block.split(/\r?\n/).filter((line) => line.startsWith('data:')).map((line) => line.slice(5).trimStart()).join('\n');
|
|
114
224
|
if (!data || data === '[DONE]')
|
|
@@ -142,20 +252,41 @@ export function parseProviderSummary(text) {
|
|
|
142
252
|
}
|
|
143
253
|
break;
|
|
144
254
|
case 'response.failed':
|
|
145
|
-
|
|
255
|
+
failed = true;
|
|
256
|
+
error ??= summaryError(isObject(event.response) ? event.response.error : null, 'response_failed');
|
|
257
|
+
break;
|
|
258
|
+
case 'response.incomplete': {
|
|
259
|
+
failed = true;
|
|
260
|
+
const details = isObject(event.response) ? event.response.incomplete_details : null;
|
|
261
|
+
error ??= { code: 'response_incomplete', param: isObject(details) ? safeUpstreamErrorId(details.reason) : null };
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
146
264
|
case 'error':
|
|
147
265
|
failed = true;
|
|
266
|
+
error ??= summaryError(isObject(event.error) ? event.error : { code: event.code, param: event.param }, 'response_failed');
|
|
148
267
|
break;
|
|
149
268
|
default:
|
|
150
269
|
break;
|
|
151
270
|
}
|
|
152
271
|
}
|
|
153
272
|
const summary = (finished.join('\n\n') || completedOutput || deltas).trim();
|
|
154
|
-
return { summary: summary || null, usage, failed: failed || !completed || !summary };
|
|
273
|
+
return { summary: summary || null, usage, failed: failed || !completed || !summary, error };
|
|
155
274
|
}
|
|
156
275
|
export function summaryWithinBounds(summary) {
|
|
157
276
|
return typeof summary === 'string' && summary.length > 0 && summary.length <= MAX_SUMMARY_CHARS;
|
|
158
277
|
}
|
|
278
|
+
export function summaryFailure(result, status) {
|
|
279
|
+
const named = result.error ? `:${result.error.code}${result.error.param ? `:${result.error.param}` : ''}` : '';
|
|
280
|
+
if (status >= 300) {
|
|
281
|
+
return {
|
|
282
|
+
code: status === 429 ? 'rate_limit_exceeded' : result.error?.code ?? 'bridge_upstream_request_failed',
|
|
283
|
+
logCode: `bridge_compaction_summary_status_${status}${named}`,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
return result.error
|
|
287
|
+
? { code: result.error.code, logCode: `bridge_compaction_summary_failed${named}` }
|
|
288
|
+
: { code: 'bridge_compaction_summary_unusable', logCode: 'bridge_compaction_summary_unusable' };
|
|
289
|
+
}
|
|
159
290
|
function wholeNumber(value) {
|
|
160
291
|
return typeof value === 'number' && Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
|
|
161
292
|
}
|
|
@@ -6,9 +6,9 @@ import { BRIDGE_OFFICIAL_ROUTE_ID } from '../bridge-contracts.js';
|
|
|
6
6
|
import { buildOfficialPassthroughHeaders, buildProviderUpstreamHeaders, rewriteResponseHeaders } from './header-policy.js';
|
|
7
7
|
import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
|
|
8
8
|
import { OPENROUTER_ONLY_REFUSAL_CODES, openRouterOnlyEnabled } from './exclusive-provider-guard.js';
|
|
9
|
-
import { ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader } from './security.js';
|
|
9
|
+
import { ensureDesktopBridgeRemoteTarget, isNoRouteUpstreamError, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode, singleBridgeHeader } from './security.js';
|
|
10
10
|
import { desktopBridgeListenOrigin } from './state.js';
|
|
11
|
-
import { buildCompactionSummaryRequest, compactEndpointResult, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, sseEvent, summaryWithinBounds, } from './compaction-adapter.js';
|
|
11
|
+
import { buildCompactionSummaryRequest, compactEndpointResult, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, safeUpstreamErrorId, sseEvent, summaryFailure, summaryWithinBounds, } from './compaction-adapter.js';
|
|
12
12
|
import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError, DesktopBridgeRequestBodyTooLargeError } from './types.js';
|
|
13
13
|
const MAX_UPSTREAM_ERROR_BODY_BYTES = 1024 * 1024;
|
|
14
14
|
function decodeBridgeRequestBody(body, encoding, maximum) {
|
|
@@ -48,10 +48,21 @@ function decodeBridgeRequestBody(body, encoding, maximum) {
|
|
|
48
48
|
throw new DesktopBridgeError('bridge_responses_body_invalid_json');
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
const encodedBodyRefusals = new Set();
|
|
52
|
+
function keepsClientEncoding(route) {
|
|
53
|
+
return route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID
|
|
54
|
+
|| (route.provider_id === 'codex-lb' && !encodedBodyRefusals.has(route.provider_id));
|
|
55
|
+
}
|
|
56
|
+
async function reencodeLikeClient(plain, encoding) {
|
|
57
|
+
const compress = zlib.zstdCompress;
|
|
58
|
+
if (encoding !== 'zstd' || !compress)
|
|
59
|
+
return null;
|
|
60
|
+
return new Promise((resolve) => compress(plain, (error, result) => resolve(error ? null : result)));
|
|
61
|
+
}
|
|
51
62
|
const RESPONSES_PATHS = ['/backend-api/codex/responses', '/api/v1/responses', '/v1/responses'];
|
|
52
63
|
function modelBodyKind(req, pathname, config) {
|
|
53
64
|
if (RESPONSES_PATHS.includes(pathname))
|
|
54
|
-
return 'responses';
|
|
65
|
+
return req.method === 'POST' ? 'responses' : null;
|
|
55
66
|
return openRouterOnlyEnabled(config) && req.method === 'POST'
|
|
56
67
|
&& RESPONSES_PATHS.some((prefix) => pathname.startsWith(`${prefix}/`)) ? 'sub-endpoint' : null;
|
|
57
68
|
}
|
|
@@ -91,14 +102,17 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
91
102
|
let body = null;
|
|
92
103
|
let payload = null;
|
|
93
104
|
let contentEncodingStripped = false;
|
|
105
|
+
let encoding = '';
|
|
106
|
+
let encodedBody = null;
|
|
94
107
|
const bodyKind = modelBodyKind(req, pathname, config);
|
|
95
108
|
if (bodyKind) {
|
|
96
109
|
const maximum = config.maxRequestBodyBytes ?? DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES;
|
|
97
110
|
body = await readBoundedBody(req, maximum);
|
|
98
|
-
|
|
111
|
+
encoding = String(req.headers['content-encoding'] || '').trim().toLowerCase();
|
|
99
112
|
let decoded = body;
|
|
100
113
|
if (encoding && encoding !== 'identity') {
|
|
101
114
|
decoded = decodeBridgeRequestBody(body, encoding, maximum);
|
|
115
|
+
encodedBody = body;
|
|
102
116
|
contentEncodingStripped = true;
|
|
103
117
|
}
|
|
104
118
|
if (bodyKind === 'responses' || decoded.length > 0) {
|
|
@@ -134,8 +148,18 @@ export async function prepareDesktopBridgeRequest(req, config) {
|
|
|
134
148
|
const compaction = bodyKind === 'responses' && requestsRemoteCompaction(payload)
|
|
135
149
|
? 'trigger'
|
|
136
150
|
: bodyKind === 'sub-endpoint' && isCompactEndpoint(req, pathname) ? 'endpoint' : null;
|
|
151
|
+
let plainBody = null;
|
|
152
|
+
const answeredByBridge = Boolean(compaction) && route.provider_id !== BRIDGE_OFFICIAL_ROUTE_ID;
|
|
153
|
+
if (encodedBody && body && !answeredByBridge && keepsClientEncoding(route)) {
|
|
154
|
+
const upstreamBody = rewrite ? await reencodeLikeClient(body, encoding) : encodedBody;
|
|
155
|
+
if (upstreamBody) {
|
|
156
|
+
plainBody = body;
|
|
157
|
+
body = upstreamBody;
|
|
158
|
+
contentEncodingStripped = false;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
137
161
|
const credential = route.provider_id === BRIDGE_OFFICIAL_ROUTE_ID ? null : await resolveCredential(config, route);
|
|
138
|
-
return { body, route, credential, contentEncodingStripped, compaction, payload };
|
|
162
|
+
return { body, route, credential, contentEncodingStripped, plainBody, compaction, payload };
|
|
139
163
|
}
|
|
140
164
|
function connectTimeout(request, config) {
|
|
141
165
|
request.once('socket', (socket) => {
|
|
@@ -172,14 +196,11 @@ function writeHttpBridgeError(res, error, req) {
|
|
|
172
196
|
});
|
|
173
197
|
res.end(JSON.stringify({ error: { type: 'sks_bridge_error', code, message: code } }));
|
|
174
198
|
}
|
|
175
|
-
|
|
176
|
-
const text = String(value ?? '').trim();
|
|
177
|
-
return /^[A-Za-z0-9_.:-]{1,64}$/.test(text) ? text : null;
|
|
178
|
-
}
|
|
179
|
-
const TRANSIENT_UPSTREAM_STATUSES = new Set([500, 502, 503, 524]);
|
|
199
|
+
const TRANSIENT_UPSTREAM_STATUSES = new Set([500, 502, 503, 520, 521, 522, 523, 524]);
|
|
180
200
|
const TRANSIENT_UPSTREAM_IDENTIFIERS = new Set(['upstream_error', 'upstream_request_timeout']);
|
|
181
201
|
export const TRANSIENT_UPSTREAM_REPLAY_LIMIT = 3;
|
|
182
202
|
const TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS = [200, 400, 800];
|
|
203
|
+
export const NO_ROUTE_REPLAY_BACKOFF_MS = [250, 500, 1_000, 2_000, 3_000, 4_000];
|
|
183
204
|
function isTransientUpstreamIdentifier(value) {
|
|
184
205
|
return Boolean(value && TRANSIENT_UPSTREAM_IDENTIFIERS.has(value));
|
|
185
206
|
}
|
|
@@ -204,6 +225,7 @@ function redactedUpstreamClientMessage(statusCode, transient) {
|
|
|
204
225
|
return 'temporary_upstream_failure';
|
|
205
226
|
return 'bridge_upstream_request_failed';
|
|
206
227
|
}
|
|
228
|
+
const UNREADABLE_BODY_HINT = /json|pars(?:e|ing)|decod|encod|compress|utf-?8/i;
|
|
207
229
|
async function readRedactedUpstreamError(response) {
|
|
208
230
|
let total = 0;
|
|
209
231
|
const chunks = [];
|
|
@@ -218,14 +240,15 @@ async function readRedactedUpstreamError(response) {
|
|
|
218
240
|
}
|
|
219
241
|
let upstreamCode = null;
|
|
220
242
|
let upstreamType = null;
|
|
243
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
221
244
|
try {
|
|
222
|
-
const parsed = JSON.parse(
|
|
245
|
+
const parsed = JSON.parse(text);
|
|
223
246
|
upstreamCode = safeUpstreamErrorId(parsed?.error?.code) ?? safeUpstreamErrorId(parsed?.detail);
|
|
224
247
|
upstreamType = safeUpstreamErrorId(parsed?.error?.type);
|
|
225
248
|
}
|
|
226
249
|
catch {
|
|
227
250
|
}
|
|
228
|
-
return { upstreamCode, upstreamType };
|
|
251
|
+
return { upstreamCode, upstreamType, unreadableBody: UNREADABLE_BODY_HINT.test(text.slice(0, 4096)) };
|
|
229
252
|
}
|
|
230
253
|
function buildRedactedUpstreamErrorBody(statusCode, upstreamCode, upstreamType, transient) {
|
|
231
254
|
return Buffer.from(JSON.stringify({
|
|
@@ -259,6 +282,7 @@ export function destroyDesktopBridgeUpstreamAgents() {
|
|
|
259
282
|
for (const agent of upstreamAgents.values())
|
|
260
283
|
agent.destroy();
|
|
261
284
|
upstreamAgents.clear();
|
|
285
|
+
encodedBodyRefusals.clear();
|
|
262
286
|
}
|
|
263
287
|
const STALE_CONNECTION_ERROR_CODES = new Set(['ECONNRESET', 'EPIPE', 'ECONNABORTED']);
|
|
264
288
|
function isStalePooledSocketFailure(request, error) {
|
|
@@ -278,6 +302,8 @@ class StalePooledSocketFailure extends Error {
|
|
|
278
302
|
}
|
|
279
303
|
class UnreachableUpstreamFailure extends StalePooledSocketFailure {
|
|
280
304
|
}
|
|
305
|
+
class EncodedBodyRefusedFailure extends StalePooledSocketFailure {
|
|
306
|
+
}
|
|
281
307
|
export async function forwardHttp(req, res, config, prepared, authenticatedLocalBaseUrl = desktopBridgeListenOrigin(config)) {
|
|
282
308
|
try {
|
|
283
309
|
const request = prepared || await prepareDesktopBridgeRequest(req, config);
|
|
@@ -322,7 +348,9 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
322
348
|
delete headers['content-encoding'];
|
|
323
349
|
const agent = upstreamAgent(remote.secure, `${remote.address}:${remote.port}`, config.idleTimeoutMs);
|
|
324
350
|
const replayable = Buffer.isBuffer(request.body);
|
|
325
|
-
|
|
351
|
+
let body = request.body;
|
|
352
|
+
let plainFallbackActive = false;
|
|
353
|
+
const attempt = (useFreshConnection, budget) => new Promise((resolve, reject) => {
|
|
326
354
|
let responseStarted = false;
|
|
327
355
|
let settled = false;
|
|
328
356
|
const abort = () => { if (!res.writableEnded)
|
|
@@ -347,7 +375,7 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
347
375
|
res.once('close', abort);
|
|
348
376
|
upstream.once('error', (error) => {
|
|
349
377
|
if (!responseStarted && isUnreachableUpstreamError(error)) {
|
|
350
|
-
if (replayable &&
|
|
378
|
+
if (replayable && (isNoRouteUpstreamError(error) ? budget.noRoute : budget.transient)) {
|
|
351
379
|
finish(new UnreachableUpstreamFailure(error));
|
|
352
380
|
return;
|
|
353
381
|
}
|
|
@@ -361,8 +389,12 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
361
389
|
});
|
|
362
390
|
upstream.once('response', (response) => {
|
|
363
391
|
const statusCode = response.statusCode || 502;
|
|
392
|
+
if (plainFallbackActive && statusCode < 400 && !official && !encodedBodyRefusals.has(provider.provider_id)) {
|
|
393
|
+
encodedBodyRefusals.add(provider.provider_id);
|
|
394
|
+
logHttpRejection({ code: 'bridge_upstream_encoded_body_refused', transport: 'http', ...(req.method === undefined ? {} : { method: req.method }), ...(req.url === undefined ? {} : { url: req.url }), provider_id: provider.provider_id });
|
|
395
|
+
}
|
|
364
396
|
if (official) {
|
|
365
|
-
if (statusCode >= 400 && TRANSIENT_UPSTREAM_STATUSES.has(statusCode) && replayable &&
|
|
397
|
+
if (statusCode >= 400 && TRANSIENT_UPSTREAM_STATUSES.has(statusCode) && replayable && budget.transient) {
|
|
366
398
|
response.resume();
|
|
367
399
|
responseStarted = true;
|
|
368
400
|
finish(new StalePooledSocketFailure(new DesktopBridgeError('bridge_upstream_transient_mislabel')));
|
|
@@ -392,9 +424,15 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
392
424
|
return;
|
|
393
425
|
}
|
|
394
426
|
if (statusCode >= 400) {
|
|
395
|
-
void readRedactedUpstreamError(response).then(({ upstreamCode, upstreamType }) => {
|
|
427
|
+
void readRedactedUpstreamError(response).then(({ upstreamCode, upstreamType, unreadableBody }) => {
|
|
428
|
+
if (budget.plainFallback && (statusCode === 415
|
|
429
|
+
|| (unreadableBody && !upstreamType && !upstreamCode && (statusCode === 400 || statusCode === 422)))) {
|
|
430
|
+
responseStarted = true;
|
|
431
|
+
finish(new EncodedBodyRefusedFailure(new DesktopBridgeError(`bridge_upstream_encoded_body_refused_${statusCode}`)));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
396
434
|
const transientMislabel = isTransientUpstreamFailure(statusCode, upstreamType, upstreamCode);
|
|
397
|
-
if (transientMislabel && replayable &&
|
|
435
|
+
if (transientMislabel && replayable && budget.transient) {
|
|
398
436
|
responseStarted = true;
|
|
399
437
|
finish(new StalePooledSocketFailure(new DesktopBridgeError(upstreamCode === 'upstream_request_timeout'
|
|
400
438
|
? 'bridge_upstream_request_timeout'
|
|
@@ -438,24 +476,46 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
438
476
|
}
|
|
439
477
|
void pipeline(response, res).then(() => finish(), finish);
|
|
440
478
|
});
|
|
441
|
-
if (
|
|
442
|
-
upstream.end(
|
|
479
|
+
if (body)
|
|
480
|
+
upstream.end(body);
|
|
443
481
|
else
|
|
444
482
|
void pipeline(req, upstream).catch((error) => { if (!responseStarted)
|
|
445
483
|
finish(error); });
|
|
446
484
|
});
|
|
447
|
-
let
|
|
485
|
+
let transientReplays = replayable ? TRANSIENT_UPSTREAM_REPLAY_LIMIT : 0;
|
|
486
|
+
let noRouteWaits = replayable ? NO_ROUTE_REPLAY_BACKOFF_MS.length : 0;
|
|
448
487
|
let useFreshConnection = false;
|
|
449
488
|
for (;;) {
|
|
450
489
|
try {
|
|
451
|
-
await attempt(useFreshConnection,
|
|
490
|
+
await attempt(useFreshConnection, {
|
|
491
|
+
transient: transientReplays > 0,
|
|
492
|
+
noRoute: noRouteWaits > 0,
|
|
493
|
+
plainFallback: Boolean(request.plainBody) && !plainFallbackActive,
|
|
494
|
+
});
|
|
452
495
|
return;
|
|
453
496
|
}
|
|
454
497
|
catch (error) {
|
|
455
|
-
if (!(error instanceof StalePooledSocketFailure)
|
|
498
|
+
if (!(error instanceof StalePooledSocketFailure))
|
|
456
499
|
throw error;
|
|
457
|
-
|
|
458
|
-
if (error instanceof
|
|
500
|
+
let backoffMs = 0;
|
|
501
|
+
if (error instanceof EncodedBodyRefusedFailure) {
|
|
502
|
+
body = request.plainBody;
|
|
503
|
+
plainFallbackActive = true;
|
|
504
|
+
headers['content-length'] = String(body.length);
|
|
505
|
+
delete headers['content-encoding'];
|
|
506
|
+
}
|
|
507
|
+
else if (error instanceof UnreachableUpstreamFailure) {
|
|
508
|
+
const noRoute = isNoRouteUpstreamError(error.reason);
|
|
509
|
+
if ((noRoute ? noRouteWaits : transientReplays) <= 0)
|
|
510
|
+
throw error;
|
|
511
|
+
if (noRoute) {
|
|
512
|
+
backoffMs = NO_ROUTE_REPLAY_BACKOFF_MS[NO_ROUTE_REPLAY_BACKOFF_MS.length - noRouteWaits];
|
|
513
|
+
noRouteWaits -= 1;
|
|
514
|
+
}
|
|
515
|
+
else {
|
|
516
|
+
backoffMs = TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS[TRANSIENT_UPSTREAM_REPLAY_LIMIT - transientReplays];
|
|
517
|
+
transientReplays -= 1;
|
|
518
|
+
}
|
|
459
519
|
const staleAddress = remote.address;
|
|
460
520
|
await refreshDesktopBridgeRemoteTarget(remote, config.remoteLookup);
|
|
461
521
|
agent.destroy();
|
|
@@ -468,6 +528,10 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
468
528
|
});
|
|
469
529
|
}
|
|
470
530
|
else {
|
|
531
|
+
if (transientReplays <= 0)
|
|
532
|
+
throw error;
|
|
533
|
+
backoffMs = TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS[TRANSIENT_UPSTREAM_REPLAY_LIMIT - transientReplays];
|
|
534
|
+
transientReplays -= 1;
|
|
471
535
|
if (!useFreshConnection) {
|
|
472
536
|
agent.destroy();
|
|
473
537
|
upstreamAgents.delete(`${remote.secure ? 'https' : 'http'}:${remote.address}:${remote.port}`);
|
|
@@ -480,9 +544,6 @@ export async function forwardHttp(req, res, config, prepared, authenticatedLocal
|
|
|
480
544
|
});
|
|
481
545
|
}
|
|
482
546
|
useFreshConnection = true;
|
|
483
|
-
const backoffIndex = TRANSIENT_UPSTREAM_REPLAY_LIMIT - remainingReplays - 1;
|
|
484
|
-
const backoffMs = TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS[Math.max(0, backoffIndex)]
|
|
485
|
-
?? TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS[TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS.length - 1];
|
|
486
547
|
if (backoffMs)
|
|
487
548
|
await delay(backoffMs);
|
|
488
549
|
}
|
|
@@ -496,8 +557,9 @@ const MAX_COMPACTION_SUMMARY_RESPONSE_BYTES = 32 * 1024 * 1024;
|
|
|
496
557
|
const COMPACTION_KEEPALIVE_MS = 15_000;
|
|
497
558
|
export async function requestProviderCompactionSummary(config, input, body, onAccepted = () => undefined) {
|
|
498
559
|
const remote = input.provider.remote;
|
|
499
|
-
let
|
|
500
|
-
|
|
560
|
+
let transientReplays = TRANSIENT_UPSTREAM_REPLAY_LIMIT;
|
|
561
|
+
let noRouteWaits = NO_ROUTE_REPLAY_BACKOFF_MS.length;
|
|
562
|
+
for (;;) {
|
|
501
563
|
await ensureDesktopBridgeRemoteTarget(remote, config.remoteLookup);
|
|
502
564
|
const target = resolveDesktopBridgeTarget(input.requestUrl, remote);
|
|
503
565
|
const headers = buildProviderUpstreamHeaders(input.clientHeaders, {
|
|
@@ -537,22 +599,31 @@ export async function requestProviderCompactionSummary(config, input, body, onAc
|
|
|
537
599
|
});
|
|
538
600
|
upstream.end(body);
|
|
539
601
|
});
|
|
540
|
-
if (result.status < 300 ||
|
|
602
|
+
if (result.status < 300 || transientReplays <= 0)
|
|
541
603
|
return result;
|
|
542
604
|
const { upstreamCode, upstreamType } = upstreamErrorIds(result.body);
|
|
543
605
|
if (!isTransientUpstreamFailure(result.status, upstreamType, upstreamCode))
|
|
544
606
|
return result;
|
|
607
|
+
await delay(TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS[TRANSIENT_UPSTREAM_REPLAY_LIMIT - transientReplays]);
|
|
608
|
+
transientReplays -= 1;
|
|
545
609
|
}
|
|
546
610
|
catch (error) {
|
|
547
|
-
if (
|
|
611
|
+
if (error instanceof DesktopBridgeError)
|
|
612
|
+
throw error;
|
|
613
|
+
const noRoute = isNoRouteUpstreamError(error);
|
|
614
|
+
if ((noRoute ? noRouteWaits : transientReplays) <= 0)
|
|
548
615
|
throw error;
|
|
549
616
|
if (isUnreachableUpstreamError(error))
|
|
550
617
|
await refreshDesktopBridgeRemoteTarget(remote, config.remoteLookup);
|
|
618
|
+
if (noRoute) {
|
|
619
|
+
await delay(NO_ROUTE_REPLAY_BACKOFF_MS[NO_ROUTE_REPLAY_BACKOFF_MS.length - noRouteWaits]);
|
|
620
|
+
noRouteWaits -= 1;
|
|
621
|
+
}
|
|
622
|
+
else {
|
|
623
|
+
await delay(TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS[TRANSIENT_UPSTREAM_REPLAY_LIMIT - transientReplays]);
|
|
624
|
+
transientReplays -= 1;
|
|
625
|
+
}
|
|
551
626
|
}
|
|
552
|
-
remaining -= 1;
|
|
553
|
-
const backoffMs = TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS[Math.min(attempt, TRANSIENT_UPSTREAM_REPLAY_BACKOFF_MS.length - 1)];
|
|
554
|
-
if (backoffMs)
|
|
555
|
-
await delay(backoffMs);
|
|
556
627
|
}
|
|
557
628
|
}
|
|
558
629
|
function upstreamErrorIds(body) {
|
|
@@ -593,9 +664,10 @@ async function answerCompactionWithProviderSummary(req, res, config, input) {
|
|
|
593
664
|
});
|
|
594
665
|
try {
|
|
595
666
|
const call = await requestProviderCompactionSummary(config, input, body, startStream);
|
|
667
|
+
const parsed = parseProviderSummary(call.body.toString('utf8'));
|
|
596
668
|
if (call.status >= 300) {
|
|
597
669
|
const { upstreamCode, upstreamType } = upstreamErrorIds(call.body);
|
|
598
|
-
log(
|
|
670
|
+
log(summaryFailure(parsed, call.status).logCode, call.status);
|
|
599
671
|
const transient = isTransientUpstreamFailure(call.status, upstreamType, upstreamCode);
|
|
600
672
|
const clientStatus = transient ? 503 : call.status;
|
|
601
673
|
const errorBody = buildRedactedUpstreamErrorBody(call.status, upstreamCode, upstreamType, transient);
|
|
@@ -606,12 +678,12 @@ async function answerCompactionWithProviderSummary(req, res, config, input) {
|
|
|
606
678
|
res.end(errorBody);
|
|
607
679
|
return;
|
|
608
680
|
}
|
|
609
|
-
const parsed = parseProviderSummary(call.body.toString('utf8'));
|
|
610
681
|
const summary = parsed.failed || !summaryWithinBounds(parsed.summary) ? null : parsed.summary;
|
|
611
682
|
if (!summary) {
|
|
612
|
-
|
|
683
|
+
const failure = summaryFailure(parsed, call.status);
|
|
684
|
+
log(failure.logCode, call.status);
|
|
613
685
|
if (streaming) {
|
|
614
|
-
res.end(sseEvent(compactionFailedEvent(responseId, publicModel,
|
|
686
|
+
res.end(sseEvent(compactionFailedEvent(responseId, publicModel, failure.code)));
|
|
615
687
|
}
|
|
616
688
|
else {
|
|
617
689
|
writeHttpBridgeError(res, new DesktopBridgeError('bridge_compaction_summary_unusable'), req);
|
|
@@ -6,7 +6,7 @@ import { buildOfficialPassthroughWebSocketHeaders, buildProviderWebSocketHeaders
|
|
|
6
6
|
import { createDesktopBridgeRejectionLogger } from './rejection-log.js';
|
|
7
7
|
import { assertDesktopBridgeRouteContext, ensureDesktopBridgeRemoteTarget, isUnreachableUpstreamError, refreshDesktopBridgeRemoteTarget, resolveAndBindDesktopBridgeRouteContext, resolveCodexSessionIdentity, resolveDesktopBridgeTarget, safeBridgeErrorCode } from './security.js';
|
|
8
8
|
import { DEFAULT_DESKTOP_BRIDGE_MAX_REQUEST_BODY_BYTES, DesktopBridgeError } from './types.js';
|
|
9
|
-
import { buildCompactionSummaryRequest, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, summaryWithinBounds, } from './compaction-adapter.js';
|
|
9
|
+
import { buildCompactionSummaryRequest, compactionCreatedEvent, compactionFailedEvent, compactionResponseEvents, expandBridgeCompactionItems, newCompactionResponseId, parseProviderSummary, requestsRemoteCompaction, ResponsesContinuationChain, summaryFailure, summaryWithinBounds, } from './compaction-adapter.js';
|
|
10
10
|
import { requestProviderCompactionSummary } from './http-forward.js';
|
|
11
11
|
const MAX_PENDING_MESSAGES = 256;
|
|
12
12
|
const CLOSE_GRACE_MS = 1_000;
|
|
@@ -53,6 +53,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
53
53
|
const pending = [];
|
|
54
54
|
let closeTimer = null;
|
|
55
55
|
let handshakeTimer = null;
|
|
56
|
+
const chain = new ResponsesContinuationChain(maxBytes);
|
|
56
57
|
const initialTimer = setTimeout(closeUnbound, config.websocketInitialCreateTimeoutMs ?? INITIAL_CREATE_TIMEOUT_MS);
|
|
57
58
|
initialTimer.unref();
|
|
58
59
|
const localSocket = socket;
|
|
@@ -118,8 +119,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
118
119
|
if (typeof model !== 'string' || !model.trim())
|
|
119
120
|
throw new DesktopBridgeError('bridge_websocket_model_required');
|
|
120
121
|
const nextIdentity = resolveCodexSessionIdentity(req.headers, message.create);
|
|
121
|
-
if (bound &&
|
|
122
|
-
|| (nextIdentity.session_id && nextIdentity.session_id !== identity.session_id))) {
|
|
122
|
+
if (bound && nextIdentity.thread_id && nextIdentity.thread_id !== identity.thread_id) {
|
|
123
123
|
throw new DesktopBridgeError('bridge_codex_session_identity_conflict');
|
|
124
124
|
}
|
|
125
125
|
identity = bound ? withLineage(identity, nextIdentity) : nextIdentity;
|
|
@@ -141,6 +141,7 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
141
141
|
}
|
|
142
142
|
if (route.upstream_model !== model || expanded)
|
|
143
143
|
message.data = Buffer.from(JSON.stringify(create));
|
|
144
|
+
message.routed = create;
|
|
144
145
|
}
|
|
145
146
|
async function answerCompaction(create) {
|
|
146
147
|
const route = bound;
|
|
@@ -150,7 +151,13 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
150
151
|
const credential = await config.resolveProviderCredential(provider.provider_id, provider.credential_generation);
|
|
151
152
|
const responseId = newCompactionResponseId();
|
|
152
153
|
const send = (event) => relay(client, Buffer.from(JSON.stringify(event)), false);
|
|
153
|
-
const
|
|
154
|
+
const input = chain.fullInput(create);
|
|
155
|
+
if (!input) {
|
|
156
|
+
logRejection({ code: 'bridge_compaction_history_unavailable', transport: 'websocket', provider_id: provider.provider_id, public_model: route.public_model, ...(req.url ? { url: req.url } : {}) });
|
|
157
|
+
send(compactionFailedEvent(responseId, route.public_model, 'bridge_compaction_history_unavailable'));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const body = Buffer.from(JSON.stringify(buildCompactionSummaryRequest({ ...create, input }, route.upstream_model)));
|
|
154
161
|
let accepted = false;
|
|
155
162
|
try {
|
|
156
163
|
const call = await requestProviderCompactionSummary(config, { provider, credential, clientHeaders: req.headers, requestUrl: req.url || '/' }, body, () => {
|
|
@@ -161,15 +168,11 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
161
168
|
});
|
|
162
169
|
if (stopped)
|
|
163
170
|
return;
|
|
164
|
-
if (call.status >= 300) {
|
|
165
|
-
logRejection({ code: `bridge_compaction_summary_status_${call.status}`, transport: 'websocket', ...(req.url ? { url: req.url } : {}) });
|
|
166
|
-
send(compactionFailedEvent(responseId, route.public_model, call.status === 429 ? 'rate_limit_exceeded' : 'bridge_upstream_request_failed'));
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
171
|
const parsed = parseProviderSummary(call.body.toString('utf8'));
|
|
170
|
-
if (parsed.failed || !summaryWithinBounds(parsed.summary)) {
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
if (call.status >= 300 || parsed.failed || !summaryWithinBounds(parsed.summary)) {
|
|
173
|
+
const failure = summaryFailure(parsed, call.status);
|
|
174
|
+
logRejection({ code: failure.logCode, transport: 'websocket', status: call.status, provider_id: provider.provider_id, public_model: route.public_model, ...(req.url ? { url: req.url } : {}) });
|
|
175
|
+
send(compactionFailedEvent(responseId, route.public_model, failure.code));
|
|
173
176
|
return;
|
|
174
177
|
}
|
|
175
178
|
const events = compactionResponseEvents(responseId, route.public_model, parsed.summary, parsed.usage);
|
|
@@ -244,8 +247,10 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
244
247
|
peer.on('message', (data, binary) => {
|
|
245
248
|
if (stopped)
|
|
246
249
|
return;
|
|
250
|
+
const frame = bytes(data);
|
|
251
|
+
chain.received(frame);
|
|
247
252
|
try {
|
|
248
|
-
relay(client,
|
|
253
|
+
relay(client, frame, binary);
|
|
249
254
|
}
|
|
250
255
|
catch (error) {
|
|
251
256
|
fail(error);
|
|
@@ -291,8 +296,11 @@ export function forwardResponsesWebSocket(req, socket, head, config) {
|
|
|
291
296
|
return;
|
|
292
297
|
if (message.compaction)
|
|
293
298
|
await answerCompaction(message.compaction);
|
|
294
|
-
else
|
|
299
|
+
else {
|
|
295
300
|
relay(upstream, message.data, message.binary);
|
|
301
|
+
if (message.routed && bound?.provider_id !== BRIDGE_OFFICIAL_ROUTE_ID)
|
|
302
|
+
chain.sent(message.routed, message.data);
|
|
303
|
+
}
|
|
296
304
|
pending.shift();
|
|
297
305
|
pendingBytes -= message.originalBytes;
|
|
298
306
|
}
|
|
@@ -115,7 +115,7 @@ function codexMetadataObject(value, source) {
|
|
|
115
115
|
}
|
|
116
116
|
return parsed;
|
|
117
117
|
}
|
|
118
|
-
function oneCodexIdentity(values
|
|
118
|
+
function oneCodexIdentity(values) {
|
|
119
119
|
const canonical = values
|
|
120
120
|
.filter((value) => value !== undefined && value !== null && value !== '')
|
|
121
121
|
.map(canonicalSessionId);
|
|
@@ -123,6 +123,17 @@ function oneCodexIdentity(values, kind) {
|
|
|
123
123
|
throw new DesktopBridgeError('bridge_codex_session_identity_conflict');
|
|
124
124
|
return canonical[0] || null;
|
|
125
125
|
}
|
|
126
|
+
function firstSessionId(values) {
|
|
127
|
+
for (const value of values) {
|
|
128
|
+
if (value === undefined || value === null || value === '')
|
|
129
|
+
continue;
|
|
130
|
+
try {
|
|
131
|
+
return canonicalSessionId(value);
|
|
132
|
+
}
|
|
133
|
+
catch { }
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
126
137
|
export function resolveCodexSessionIdentity(headers, payload = null) {
|
|
127
138
|
const turnMetadataValue = singleBridgeHeader(headers, 'x-codex-turn-metadata');
|
|
128
139
|
const turnMetadata = turnMetadataValue === null ? null : codexMetadataObject(turnMetadataValue, 'header');
|
|
@@ -132,12 +143,12 @@ export function resolveCodexSessionIdentity(headers, payload = null) {
|
|
|
132
143
|
singleBridgeHeader(headers, 'thread-id'),
|
|
133
144
|
turnMetadata?.thread_id,
|
|
134
145
|
clientMetadata?.thread_id,
|
|
135
|
-
]
|
|
136
|
-
const sessionId =
|
|
137
|
-
singleBridgeHeader(headers, 'session-id'),
|
|
138
|
-
turnMetadata?.session_id,
|
|
146
|
+
]);
|
|
147
|
+
const sessionId = firstSessionId([
|
|
139
148
|
clientMetadata?.session_id,
|
|
140
|
-
|
|
149
|
+
turnMetadata?.session_id,
|
|
150
|
+
singleBridgeHeader(headers, 'session-id'),
|
|
151
|
+
]);
|
|
141
152
|
if (sessionId && !threadId)
|
|
142
153
|
throw new DesktopBridgeError('bridge_codex_thread_id_missing');
|
|
143
154
|
const nested = nestedTurnMetadata(clientMetadata);
|
|
@@ -348,8 +359,22 @@ export function assertDesktopBridgeRouteContext(request, config) {
|
|
|
348
359
|
return route;
|
|
349
360
|
}
|
|
350
361
|
export async function resolveAndBindDesktopBridgeRouteContext(request, config) {
|
|
362
|
+
const pinsRead = config.providerSessionPins;
|
|
363
|
+
let steady = null;
|
|
364
|
+
try {
|
|
365
|
+
steady = assertDesktopBridgeRouteContext(request, config);
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
steady = null;
|
|
369
|
+
}
|
|
370
|
+
const steadyPin = steady?.session_pin;
|
|
371
|
+
if (steady && !steadyPin)
|
|
372
|
+
return steady;
|
|
373
|
+
const persisted = steadyPin ? pinsRead.find((entry) => entry.thread_id === steadyPin.thread_id) : undefined;
|
|
374
|
+
if (steady && steadyPin && persisted && sameSessionPin(persisted, steadyPin))
|
|
375
|
+
return steady;
|
|
351
376
|
return withSessionPinMutation(config, async () => {
|
|
352
|
-
const route = assertDesktopBridgeRouteContext(request, config);
|
|
377
|
+
const route = steady && config.providerSessionPins === pinsRead ? steady : assertDesktopBridgeRouteContext(request, config);
|
|
353
378
|
const pin = route.session_pin;
|
|
354
379
|
if (!pin)
|
|
355
380
|
return route;
|
|
@@ -455,7 +480,10 @@ export async function resolveDesktopBridgeRemoteTarget(raw, lookup = defaultLook
|
|
|
455
480
|
if (!loopback && addresses.some((row) => FORBIDDEN_REMOTE_ADDRESSES.check(row.address, row.family === 4 ? 'ipv4' : 'ipv6'))) {
|
|
456
481
|
throw new DesktopBridgeError('bridge_remote_dns_private_address');
|
|
457
482
|
}
|
|
483
|
+
const avoided = options.avoidAddress ? addresses.find((row) => row.address === options.avoidAddress) : undefined;
|
|
458
484
|
const selected = (options.preferAddress ? addresses.find((row) => row.address === options.preferAddress) : undefined)
|
|
485
|
+
?? (avoided ? addresses.find((row) => row.family !== avoided.family) : undefined)
|
|
486
|
+
?? (options.preferFamily ? addresses.find((row) => row.family === options.preferFamily && row.address !== options.avoidAddress) : undefined)
|
|
459
487
|
?? addresses.find((row) => row.address !== options.avoidAddress)
|
|
460
488
|
?? addresses[0];
|
|
461
489
|
if (!selected)
|
|
@@ -498,6 +526,11 @@ export function isUnreachableUpstreamError(error) {
|
|
|
498
526
|
const code = error?.code;
|
|
499
527
|
return typeof code === 'string' && UNREACHABLE_UPSTREAM_ERROR_CODES.has(code);
|
|
500
528
|
}
|
|
529
|
+
const NO_ROUTE_UPSTREAM_ERROR_CODES = new Set(['ENETUNREACH', 'EHOSTUNREACH', 'ENETDOWN', 'EHOSTDOWN', 'EADDRNOTAVAIL', 'EAFNOSUPPORT']);
|
|
530
|
+
export function isNoRouteUpstreamError(error) {
|
|
531
|
+
const code = error?.code;
|
|
532
|
+
return typeof code === 'string' && NO_ROUTE_UPSTREAM_ERROR_CODES.has(code);
|
|
533
|
+
}
|
|
501
534
|
export const REMOTE_REFRESH_COOLDOWN_MS = 5_000;
|
|
502
535
|
export const REMOTE_TARGET_TTL_MS = 5 * 60_000;
|
|
503
536
|
export async function refreshDesktopBridgeRemoteTarget(remote, lookup = defaultLookup, reason = 'unreachable') {
|
|
@@ -512,7 +545,7 @@ export async function refreshDesktopBridgeRemoteTarget(remote, lookup = defaultL
|
|
|
512
545
|
let resolvedAt = state.resolvedAt;
|
|
513
546
|
try {
|
|
514
547
|
const fresh = await resolveDesktopBridgeRemoteTarget(remote.baseUrl, lookup, reason === 'stale'
|
|
515
|
-
? { preferAddress: remote.address }
|
|
548
|
+
? { preferAddress: remote.address, preferFamily: remote.family }
|
|
516
549
|
: reason === 'unreachable' ? { avoidAddress: remote.address } : {});
|
|
517
550
|
const changed = remote.unresolved === true || fresh.address !== remote.address || fresh.family !== remote.family;
|
|
518
551
|
remote.address = fresh.address;
|
|
@@ -291,6 +291,9 @@ export async function startPreparedDesktopBridge(input, options = {}) {
|
|
|
291
291
|
if (!socket.destroyed)
|
|
292
292
|
socket.end('HTTP/1.1 400 Bad Request\r\nConnection: close\r\n\r\n');
|
|
293
293
|
});
|
|
294
|
+
server.on('drop', () => {
|
|
295
|
+
logBridgeRejection({ code: 'bridge_connection_limit_reached', transport: 'http' });
|
|
296
|
+
});
|
|
294
297
|
server.on('upgrade', (req, socket, head) => {
|
|
295
298
|
try {
|
|
296
299
|
assertLoopbackPeer(req.socket.remoteAddress);
|
package/dist/core/fsx.js
CHANGED
|
@@ -693,9 +693,72 @@ export async function terminateProcessTree(pid, child) {
|
|
|
693
693
|
signalProcessTree(pid, 'SIGTERM', child);
|
|
694
694
|
if (await waitForProcessTreeExit(pid, 1_000))
|
|
695
695
|
return;
|
|
696
|
+
await killProcessGroupDescendants(pid);
|
|
696
697
|
signalProcessTree(pid, 'SIGKILL', child);
|
|
697
698
|
await waitForProcessTreeExit(pid, 1_500);
|
|
698
699
|
}
|
|
700
|
+
async function killProcessGroupDescendants(groupId) {
|
|
701
|
+
if (!groupId || process.platform === 'win32')
|
|
702
|
+
return;
|
|
703
|
+
const members = linuxProcessGroupMembers(groupId)
|
|
704
|
+
.filter((member) => member.pid !== groupId)
|
|
705
|
+
.sort((left, right) => right.depth - left.depth || right.pid - left.pid);
|
|
706
|
+
for (const member of members) {
|
|
707
|
+
try {
|
|
708
|
+
process.kill(member.pid, 'SIGKILL');
|
|
709
|
+
}
|
|
710
|
+
catch { }
|
|
711
|
+
}
|
|
712
|
+
if (members.length)
|
|
713
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
714
|
+
}
|
|
715
|
+
function linuxProcessGroupMembers(groupId) {
|
|
716
|
+
if (process.platform !== 'linux')
|
|
717
|
+
return [];
|
|
718
|
+
let entries;
|
|
719
|
+
try {
|
|
720
|
+
entries = fs.readdirSync('/proc');
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
return [];
|
|
724
|
+
}
|
|
725
|
+
const rows = [];
|
|
726
|
+
for (const entry of entries) {
|
|
727
|
+
if (!/^\d+$/.test(entry))
|
|
728
|
+
continue;
|
|
729
|
+
const stat = readLinuxProcessStat(Number(entry));
|
|
730
|
+
if (!stat || stat.pgrp !== groupId)
|
|
731
|
+
continue;
|
|
732
|
+
rows.push({ pid: stat.pid, ppid: stat.ppid });
|
|
733
|
+
}
|
|
734
|
+
const parents = new Map(rows.map((row) => [row.pid, row.ppid]));
|
|
735
|
+
return rows.map((row) => {
|
|
736
|
+
let depth = 0;
|
|
737
|
+
const seen = new Set();
|
|
738
|
+
let current = row.ppid;
|
|
739
|
+
while (parents.has(current) && !seen.has(current)) {
|
|
740
|
+
seen.add(current);
|
|
741
|
+
depth += 1;
|
|
742
|
+
current = parents.get(current);
|
|
743
|
+
}
|
|
744
|
+
return { pid: row.pid, depth };
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
function readLinuxProcessStat(pid) {
|
|
748
|
+
try {
|
|
749
|
+
const text = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
750
|
+
const close = text.lastIndexOf(')');
|
|
751
|
+
if (close < 0)
|
|
752
|
+
return null;
|
|
753
|
+
const fields = text.slice(close + 1).trim().split(/\s+/);
|
|
754
|
+
const ppid = Number(fields[1]);
|
|
755
|
+
const pgrp = Number(fields[2]);
|
|
756
|
+
return Number.isInteger(ppid) && Number.isInteger(pgrp) ? { pid, ppid, pgrp } : null;
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
699
762
|
function signalProcessTree(pid, signal, child) {
|
|
700
763
|
if (pid && process.platform !== 'win32') {
|
|
701
764
|
try {
|
package/dist/core/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = '10.
|
|
1
|
+
export const PACKAGE_VERSION = '10.6.0';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sneakoscope",
|
|
3
3
|
"displayName": "ㅅㅋㅅ",
|
|
4
|
-
"version": "10.
|
|
4
|
+
"version": "10.6.0",
|
|
5
5
|
"description": "Sneakoscope Codex (`sks`) is an open-source trust layer for Codex CLI and ChatGPT Desktop. It coordinates bounded AI coding agents, records machine-verifiable evidence, preserves project memory, and blocks release claims that are not supported by current tests or artifacts. Search visibility outcomes are measured separately; SKS does not promise rankings or traffic.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"homepage": "https://github.com/mandarange/Sneakoscope-Codex#readme",
|
|
@@ -193,8 +193,8 @@
|
|
|
193
193
|
"dependencies": {
|
|
194
194
|
"@modelcontextprotocol/client": "2.3.1",
|
|
195
195
|
"@modelcontextprotocol/server": "2.3.1",
|
|
196
|
-
"@openai/codex-sdk": "0.
|
|
197
|
-
"smol-toml": "^1.9.
|
|
196
|
+
"@openai/codex-sdk": "0.163.0",
|
|
197
|
+
"smol-toml": "^1.9.1",
|
|
198
198
|
"typescript": "^6.0.3",
|
|
199
199
|
"ws": "^8.22.0"
|
|
200
200
|
},
|