@opengeni/codex 0.2.10 → 0.2.13
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/dist/fetch.d.ts +4 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +50 -23
- package/dist/index.js.map +1 -1
- package/dist/mcp-sanitize.d.ts +4 -3
- package/dist/opaque-artifact.d.ts +4 -0
- package/dist/request-context.d.ts +6 -0
- package/package.json +1 -1
- package/src/fetch.ts +24 -23
- package/src/index.ts +1 -0
- package/src/mcp-sanitize.ts +17 -12
- package/src/normalize.ts +1 -1
- package/src/opaque-artifact.ts +29 -0
- package/src/request-context.ts +7 -0
package/dist/mcp-sanitize.d.ts
CHANGED
|
@@ -25,9 +25,10 @@ export declare function remapToolCallRequestBody(body: string, mapper: ToolNameM
|
|
|
25
25
|
* to the MCP server's original. Only the POST request/response is buffered; the
|
|
26
26
|
* long-lived GET notification SSE stream is passed through untouched.
|
|
27
27
|
*
|
|
28
|
-
*
|
|
28
|
+
* An optional `namespaceSink` Set accumulates the ORIGINAL-dotted
|
|
29
29
|
* connector namespaces seen across every tools/list this turn (captured before the
|
|
30
|
-
* dot is sanitized away). The
|
|
31
|
-
* turn
|
|
30
|
+
* dot is sanitized away). The runtime reads the live by-reference Set only to keep
|
|
31
|
+
* this turn's `tool_search` description accurate; it is never persisted or used
|
|
32
|
+
* for inference selection.
|
|
32
33
|
*/
|
|
33
34
|
export declare function codexAppsSanitizingFetch(base?: FetchLike, namespaceSink?: Set<string>): FetchLike;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Stable, content-hiding identity for one opaque provider artifact. */
|
|
2
|
+
export declare function opaqueProviderArtifactFingerprint(item: unknown): string | null;
|
|
3
|
+
/** Exact opaque artifacts present in one normalized provider input array. */
|
|
4
|
+
export declare function opaqueProviderArtifactFingerprints(input: unknown): string[];
|
|
@@ -49,6 +49,10 @@ export type CodexModelRequestEvent = {
|
|
|
49
49
|
status?: number;
|
|
50
50
|
willRetry?: boolean;
|
|
51
51
|
};
|
|
52
|
+
export type CodexRequestOpaqueArtifacts = {
|
|
53
|
+
requestId: string;
|
|
54
|
+
fingerprints: readonly string[];
|
|
55
|
+
};
|
|
52
56
|
export type CodexRequestContext = {
|
|
53
57
|
clientVersion: string;
|
|
54
58
|
/**
|
|
@@ -81,6 +85,8 @@ export type CodexRequestContext = {
|
|
|
81
85
|
responseTimeoutPolicy?: Partial<CodexResponseTimeoutPolicy>;
|
|
82
86
|
/** Worker-owned durable audit sink; payloads never contain request bodies or auth. */
|
|
83
87
|
onModelRequestEvent?: (event: CodexModelRequestEvent) => Promise<void> | void;
|
|
88
|
+
/** Exact opaque artifacts on the normalized wire request, never their ciphertext. */
|
|
89
|
+
onRequestOpaqueArtifacts?: (artifacts: CodexRequestOpaqueArtifacts) => void;
|
|
84
90
|
/** Stable request identity supplied by the owning durable execution. */
|
|
85
91
|
nextRequestId?: () => string;
|
|
86
92
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@opengeni/codex",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"description": "ChatGPT/Codex subscription auth + transport: device-code login, token refresh, and the Responses-backend fetch. Pure HTTP + transforms; no database dependency.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
package/src/fetch.ts
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { randomUUID } from "node:crypto";
|
|
14
14
|
import { CODEX_ORIGINATOR } from "./constants";
|
|
15
15
|
import { normalizeCodexRequestBody } from "./normalize";
|
|
16
|
+
import { opaqueProviderArtifactFingerprints } from "./opaque-artifact";
|
|
16
17
|
import {
|
|
17
18
|
codexRequestStorage,
|
|
18
19
|
type CodexModelRequestEvent,
|
|
@@ -489,6 +490,7 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
489
490
|
// summarizer) still gets a single JSON Response back.
|
|
490
491
|
let callerWantsStream = true;
|
|
491
492
|
let model: string | undefined;
|
|
493
|
+
let requestOpaqueArtifacts: string[] = [];
|
|
492
494
|
const nextInit: RequestInit = { ...init, headers };
|
|
493
495
|
if (typeof init?.body === "string") {
|
|
494
496
|
try {
|
|
@@ -497,26 +499,23 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
497
499
|
const normalized = normalizeCodexRequestBody(parsed, ctx.resolveModel);
|
|
498
500
|
model = typeof normalized.model === "string" ? normalized.model : undefined;
|
|
499
501
|
nextInit.body = JSON.stringify(normalized);
|
|
502
|
+
requestOpaqueArtifacts = opaqueProviderArtifactFingerprints(normalized.input);
|
|
500
503
|
} catch {
|
|
501
504
|
/* leave unparseable bodies untouched (already copied from init) */
|
|
502
505
|
}
|
|
503
506
|
}
|
|
507
|
+
ctx.onRequestOpaqueArtifacts?.({ requestId, fingerprints: requestOpaqueArtifacts });
|
|
504
508
|
headers.set(
|
|
505
509
|
"Idempotency-Key",
|
|
506
510
|
authenticationAttempt === 0 ? requestId : `${requestId}:auth-${authenticationAttempt}`,
|
|
507
511
|
);
|
|
508
512
|
if (process.env.CODEX_DEBUG) {
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
console.error(
|
|
518
|
-
`[codex-debug] POST ${rewritten} stream=${callerWantsStream} bodyKeys=[${keys.join(",")}]`,
|
|
519
|
-
);
|
|
513
|
+
console.error("[codex-debug] request dispatched", {
|
|
514
|
+
method: "POST",
|
|
515
|
+
origin: "codex-subscription",
|
|
516
|
+
route: "codex_responses",
|
|
517
|
+
stream: callerWantsStream,
|
|
518
|
+
});
|
|
520
519
|
}
|
|
521
520
|
let res: Response;
|
|
522
521
|
transportAttempt += 1;
|
|
@@ -583,12 +582,13 @@ export function codexSubscriptionFetch(base: FetchLike = globalThis.fetch): Fetc
|
|
|
583
582
|
ctx.onUsageHeaders?.(usage);
|
|
584
583
|
}
|
|
585
584
|
if (process.env.CODEX_DEBUG && !res.ok) {
|
|
586
|
-
// Never log provider bodies: they can contain
|
|
587
|
-
// account
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
585
|
+
// Never log provider bodies, identifiers, or headers: they can contain
|
|
586
|
+
// request-derived or account content. A bounded status is sufficient.
|
|
587
|
+
console.error("[codex-debug] request failed", {
|
|
588
|
+
origin: "codex-subscription",
|
|
589
|
+
route: "codex_responses",
|
|
590
|
+
status: res.status,
|
|
591
|
+
});
|
|
592
592
|
}
|
|
593
593
|
// The codex backend leaves the terminal event's response.output empty and
|
|
594
594
|
// delivers the assistant items via output_item.done events instead. The
|
|
@@ -1012,9 +1012,9 @@ function codexSseFailureProjection(
|
|
|
1012
1012
|
* A provider terminal carried inside an accepted HTTP-200 stream. The OpenAI
|
|
1013
1013
|
* SDK cannot turn that late terminal into a non-2xx APIError because headers
|
|
1014
1014
|
* have already been accepted, so the body transform throws this equivalent
|
|
1015
|
-
* bounded shape. Provider-supplied message/param text
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
1015
|
+
* bounded shape. Provider-supplied message/param text remains exact within the
|
|
1016
|
+
* explicit terminal-field byte contract; retry classification is additive and
|
|
1017
|
+
* never substitutes for the source diagnostic.
|
|
1018
1018
|
*/
|
|
1019
1019
|
export class CodexStreamingTerminalError extends Error {
|
|
1020
1020
|
readonly status: number;
|
|
@@ -1026,8 +1026,8 @@ export class CodexStreamingTerminalError extends Error {
|
|
|
1026
1026
|
readonly headers: Headers;
|
|
1027
1027
|
readonly error: Record<string, unknown>;
|
|
1028
1028
|
|
|
1029
|
-
constructor(projection: CodexSseFailureProjection
|
|
1030
|
-
super(
|
|
1029
|
+
constructor(projection: CodexSseFailureProjection) {
|
|
1030
|
+
super(projection.error.message);
|
|
1031
1031
|
this.name = "CodexStreamingTerminalError";
|
|
1032
1032
|
this.status = projection.status;
|
|
1033
1033
|
this.code = projection.error.code;
|
|
@@ -1045,6 +1045,8 @@ export class CodexStreamingTerminalError extends Error {
|
|
|
1045
1045
|
this.error = {
|
|
1046
1046
|
type: projection.error.type,
|
|
1047
1047
|
code: projection.error.code,
|
|
1048
|
+
message: projection.error.message,
|
|
1049
|
+
...(projection.error.param ? { param: projection.error.param } : {}),
|
|
1048
1050
|
...(projection.error.event_type ? { event_type: projection.error.event_type } : {}),
|
|
1049
1051
|
...(projection.error.response_id ? { response_id: projection.error.response_id } : {}),
|
|
1050
1052
|
...(projection.error.response_status
|
|
@@ -1068,7 +1070,6 @@ function codexSseFailureError(
|
|
|
1068
1070
|
): CodexStreamingTerminalError {
|
|
1069
1071
|
return new CodexStreamingTerminalError(
|
|
1070
1072
|
codexSseFailureProjection(source, rawError, fallbackCode, publicMessage, metadata),
|
|
1071
|
-
publicMessage,
|
|
1072
1073
|
);
|
|
1073
1074
|
}
|
|
1074
1075
|
|
package/src/index.ts
CHANGED
package/src/mcp-sanitize.ts
CHANGED
|
@@ -105,7 +105,7 @@ export class ToolNameMapper {
|
|
|
105
105
|
/**
|
|
106
106
|
* Drop bad outputSchemas + sanitize tool names on a JSON-RPC tools/list result, in place.
|
|
107
107
|
*
|
|
108
|
-
*
|
|
108
|
+
* When `namespaceSink` is provided, accumulate each tool's ORIGINAL
|
|
109
109
|
* connector namespace (the segment BEFORE the first dot, e.g. `github` from
|
|
110
110
|
* `github.create_issue`) into it — captured HERE because this pass sees the original
|
|
111
111
|
* dotted name BEFORE mapper.sanitize rewrites the dot away. Only dotted names carry a
|
|
@@ -169,8 +169,10 @@ function sanitizeToolsInRpcMessage(
|
|
|
169
169
|
* leaving the original content untouched.
|
|
170
170
|
*
|
|
171
171
|
* No-op when there is no `structuredContent` — so a tools/list response (or any
|
|
172
|
-
* result without it) passes through unchanged.
|
|
173
|
-
*
|
|
172
|
+
* result without it) passes through unchanged. Valid object payloads stay intact
|
|
173
|
+
* for protocol-aware consumers. Invalid optional values (such as null) are removed
|
|
174
|
+
* after any useful value is copied into `content`, because the MCP client validates
|
|
175
|
+
* this field as an object before the tool can return. Runs after the outputSchema drop.
|
|
174
176
|
*/
|
|
175
177
|
function inlineStructuredContentInRpcMessage(message: unknown): void {
|
|
176
178
|
if (!message || typeof message !== "object") {
|
|
@@ -185,13 +187,15 @@ function inlineStructuredContentInRpcMessage(message: unknown): void {
|
|
|
185
187
|
return;
|
|
186
188
|
}
|
|
187
189
|
const structured = record.structuredContent;
|
|
188
|
-
if (structured
|
|
189
|
-
|
|
190
|
+
if (structured !== undefined && structured !== null) {
|
|
191
|
+
const text = typeof structured === "string" ? structured : JSON.stringify(structured);
|
|
192
|
+
const content = Array.isArray(record.content) ? [...record.content] : [];
|
|
193
|
+
content.push({ type: "text", text });
|
|
194
|
+
record.content = content;
|
|
195
|
+
}
|
|
196
|
+
if (typeof structured !== "object" || structured === null || Array.isArray(structured)) {
|
|
197
|
+
delete record.structuredContent;
|
|
190
198
|
}
|
|
191
|
-
const text = typeof structured === "string" ? structured : JSON.stringify(structured);
|
|
192
|
-
const content = Array.isArray(record.content) ? [...record.content] : [];
|
|
193
|
-
content.push({ type: "text", text });
|
|
194
|
-
record.content = content;
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
/** Sanitize a single JSON body (application/json MCP response). */
|
|
@@ -264,10 +268,11 @@ export function remapToolCallRequestBody(body: string, mapper: ToolNameMapper):
|
|
|
264
268
|
* to the MCP server's original. Only the POST request/response is buffered; the
|
|
265
269
|
* long-lived GET notification SSE stream is passed through untouched.
|
|
266
270
|
*
|
|
267
|
-
*
|
|
271
|
+
* An optional `namespaceSink` Set accumulates the ORIGINAL-dotted
|
|
268
272
|
* connector namespaces seen across every tools/list this turn (captured before the
|
|
269
|
-
* dot is sanitized away). The
|
|
270
|
-
* turn
|
|
273
|
+
* dot is sanitized away). The runtime reads the live by-reference Set only to keep
|
|
274
|
+
* this turn's `tool_search` description accurate; it is never persisted or used
|
|
275
|
+
* for inference selection.
|
|
271
276
|
*/
|
|
272
277
|
export function codexAppsSanitizingFetch(
|
|
273
278
|
base: FetchLike = globalThis.fetch,
|
package/src/normalize.ts
CHANGED
|
@@ -69,7 +69,7 @@ export function normalizeCodexRequestBody(
|
|
|
69
69
|
// strip every item id; PRESERVE call_id. spec §1.6 / verdict §0(b)
|
|
70
70
|
// (This also covers tool_search items: the backend accepts an id-less
|
|
71
71
|
// tool_search_call/output pair correlated by call_id — verified live — and
|
|
72
|
-
// stripping the
|
|
72
|
+
// stripping the provider-stored `tsc_…` id here sanitizes BOTH replay paths.)
|
|
73
73
|
if (Array.isArray(body.input)) {
|
|
74
74
|
for (const item of body.input as unknown[]) {
|
|
75
75
|
if (!item || typeof item !== "object") {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/** Stable, content-hiding identity for one opaque provider artifact. */
|
|
4
|
+
export function opaqueProviderArtifactFingerprint(item: unknown): string | null {
|
|
5
|
+
if (!item || typeof item !== "object") return null;
|
|
6
|
+
const record = item as Record<string, unknown>;
|
|
7
|
+
if (record.type !== "reasoning" && record.type !== "compaction") return null;
|
|
8
|
+
const providerData =
|
|
9
|
+
record.providerData && typeof record.providerData === "object"
|
|
10
|
+
? (record.providerData as Record<string, unknown>)
|
|
11
|
+
: null;
|
|
12
|
+
const ciphertext =
|
|
13
|
+
(typeof record.encrypted_content === "string" && record.encrypted_content) ||
|
|
14
|
+
(typeof record.encryptedContent === "string" && record.encryptedContent) ||
|
|
15
|
+
(typeof providerData?.encrypted_content === "string" && providerData.encrypted_content) ||
|
|
16
|
+
(typeof providerData?.encryptedContent === "string" && providerData.encryptedContent) ||
|
|
17
|
+
null;
|
|
18
|
+
if (!ciphertext) return null;
|
|
19
|
+
return `${record.type}:${createHash("sha256").update(ciphertext).digest("hex")}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Exact opaque artifacts present in one normalized provider input array. */
|
|
23
|
+
export function opaqueProviderArtifactFingerprints(input: unknown): string[] {
|
|
24
|
+
if (!Array.isArray(input)) return [];
|
|
25
|
+
return input.flatMap((item) => {
|
|
26
|
+
const fingerprint = opaqueProviderArtifactFingerprint(item);
|
|
27
|
+
return fingerprint ? [fingerprint] : [];
|
|
28
|
+
});
|
|
29
|
+
}
|
package/src/request-context.ts
CHANGED
|
@@ -62,6 +62,11 @@ export type CodexModelRequestEvent = {
|
|
|
62
62
|
willRetry?: boolean;
|
|
63
63
|
};
|
|
64
64
|
|
|
65
|
+
export type CodexRequestOpaqueArtifacts = {
|
|
66
|
+
requestId: string;
|
|
67
|
+
fingerprints: readonly string[];
|
|
68
|
+
};
|
|
69
|
+
|
|
65
70
|
export type CodexRequestContext = {
|
|
66
71
|
clientVersion: string;
|
|
67
72
|
/**
|
|
@@ -94,6 +99,8 @@ export type CodexRequestContext = {
|
|
|
94
99
|
responseTimeoutPolicy?: Partial<CodexResponseTimeoutPolicy>;
|
|
95
100
|
/** Worker-owned durable audit sink; payloads never contain request bodies or auth. */
|
|
96
101
|
onModelRequestEvent?: (event: CodexModelRequestEvent) => Promise<void> | void;
|
|
102
|
+
/** Exact opaque artifacts on the normalized wire request, never their ciphertext. */
|
|
103
|
+
onRequestOpaqueArtifacts?: (artifacts: CodexRequestOpaqueArtifacts) => void;
|
|
97
104
|
/** Stable request identity supplied by the owning durable execution. */
|
|
98
105
|
nextRequestId?: () => string;
|
|
99
106
|
/**
|