@bitkyc08/opencodex 2.20.0 → 2.22.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/AGENTS_INSTALL.md +32 -0
- package/README.md +1 -1
- package/gui/dist/assets/{index-DSK3S5HY.js → index-ClEcVlFO.js} +43 -17
- package/gui/dist/assets/index-DQsMZzI5.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +2 -28
- package/src/adapters/google.ts +31 -3
- package/src/adapters/openai-chat.ts +25 -8
- package/src/adapters/responses-tool-schema.ts +67 -0
- package/src/bridge.ts +15 -2
- package/src/claude/agents-inject.ts +2 -2
- package/src/claude/gateway-cache.ts +41 -4
- package/src/cli/claude.ts +1 -1
- package/src/cli/codex-log-guard-doctor.ts +103 -0
- package/src/cli/dispatch.ts +7 -1
- package/src/cli/help.ts +1 -1
- package/src/cli/models.ts +16 -6
- package/src/cli/observe.ts +38 -2
- package/src/cli/registry.ts +2 -1
- package/src/cli/v2.ts +34 -1
- package/src/codex/app-server-processes.ts +46 -26
- package/src/codex/catalog/effort.ts +49 -1
- package/src/codex/catalog/parsing.ts +64 -4
- package/src/codex/catalog/provider-fetch.ts +12 -0
- package/src/codex/catalog/sync.ts +14 -1
- package/src/codex/convergence.ts +2 -0
- package/src/codex/inject.ts +3 -3
- package/src/codex/log-guard/inspect.ts +506 -0
- package/src/codex/log-guard/lock.ts +150 -0
- package/src/codex/log-guard/maintenance.ts +403 -0
- package/src/codex/log-guard/path-safety.ts +39 -0
- package/src/codex/log-guard/policy.ts +44 -0
- package/src/codex/log-guard/processes.ts +205 -0
- package/src/codex/log-guard/protection.ts +489 -0
- package/src/codex/log-guard/sqlite-errors.ts +9 -0
- package/src/codex/paths.ts +5 -0
- package/src/codex/plugins-doctor.ts +1 -1
- package/src/codex/project-config-warnings.ts +2 -2
- package/src/generated/compatibility-version.json +93 -45
- package/src/images/loop.ts +15 -5
- package/src/providers/antigravity-models.ts +11 -1
- package/src/providers/model-discovery.ts +94 -6
- package/src/providers/quota.ts +159 -0
- package/src/providers/registry.ts +20 -1
- package/src/providers/slug-codec.ts +29 -0
- package/src/responses/custom-tool-compat.ts +4 -1
- package/src/responses/parser.ts +7 -1
- package/src/responses/provider-opaque-metadata.ts +73 -0
- package/src/responses/schema.ts +6 -0
- package/src/router.ts +12 -4
- package/src/routing/capability.ts +32 -17
- package/src/server/auth-cors.ts +42 -6
- package/src/server/index.ts +1 -0
- package/src/server/management/agent-settings-routes.ts +20 -2
- package/src/server/management/context.ts +15 -0
- package/src/server/management/model-routes.ts +12 -3
- package/src/server/management/storage-log-guard-routes.ts +186 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/responses/core.ts +13 -0
- package/src/server/system-env.ts +1 -1
- package/src/types.ts +24 -2
- package/src/web-search/loop.ts +21 -5
- package/gui/dist/assets/index-DF_UFrGS.css +0 -1
|
@@ -232,6 +232,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
232
232
|
agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
|
|
233
233
|
maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
|
|
234
234
|
multiAgentMode: config.multiAgentMode ?? "default",
|
|
235
|
+
keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
|
|
235
236
|
agentsEnabled: getAgentsEnabled(),
|
|
236
237
|
agentsMaxDepth: getAgentsMaxDepth(),
|
|
237
238
|
subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
|
|
@@ -246,6 +247,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
246
247
|
enabled?: unknown;
|
|
247
248
|
maxConcurrentThreadsPerSession?: unknown;
|
|
248
249
|
multiAgentMode?: unknown;
|
|
250
|
+
keepNativeChatGptOnV1?: unknown;
|
|
249
251
|
agentsEnabled?: unknown;
|
|
250
252
|
agentsMaxDepth?: unknown;
|
|
251
253
|
subagentDeveloperInstructions?: unknown;
|
|
@@ -255,17 +257,21 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
255
257
|
const wantsFlag = body.enabled !== undefined;
|
|
256
258
|
const wantsThreads = body.maxConcurrentThreadsPerSession !== undefined;
|
|
257
259
|
const wantsMode = body.multiAgentMode !== undefined;
|
|
260
|
+
const wantsKeepNative = body.keepNativeChatGptOnV1 !== undefined;
|
|
258
261
|
const wantsAgentsEnabled = body.agentsEnabled !== undefined;
|
|
259
262
|
const wantsMaxDepth = body.agentsMaxDepth !== undefined;
|
|
260
263
|
const wantsSubagentInstructions = body.subagentDeveloperInstructions !== undefined;
|
|
261
264
|
const wantsModeHintText = body.multiAgentModeHintText !== undefined;
|
|
262
|
-
if (!wantsFlag && !wantsThreads && !wantsMode && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText) {
|
|
263
|
-
return jsonResponse({ error: "body must set enabled, multiAgentMode, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, and/or multiAgentModeHintText" }, 400);
|
|
265
|
+
if (!wantsFlag && !wantsThreads && !wantsMode && !wantsKeepNative && !wantsAgentsEnabled && !wantsMaxDepth && !wantsSubagentInstructions && !wantsModeHintText) {
|
|
266
|
+
return jsonResponse({ error: "body must set enabled, multiAgentMode, keepNativeChatGptOnV1, maxConcurrentThreadsPerSession, agentsEnabled, agentsMaxDepth, subagentDeveloperInstructions, and/or multiAgentModeHintText" }, 400);
|
|
264
267
|
}
|
|
265
268
|
if (wantsFlag && typeof body.enabled !== "boolean") return jsonResponse({ error: "body.enabled must be a boolean" }, 400);
|
|
266
269
|
if (wantsMode && body.multiAgentMode !== "v1" && body.multiAgentMode !== "default" && body.multiAgentMode !== "v2") {
|
|
267
270
|
return jsonResponse({ error: "body.multiAgentMode must be 'v1', 'default', or 'v2'" }, 400);
|
|
268
271
|
}
|
|
272
|
+
if (wantsKeepNative && typeof body.keepNativeChatGptOnV1 !== "boolean") {
|
|
273
|
+
return jsonResponse({ error: "body.keepNativeChatGptOnV1 must be a boolean" }, 400);
|
|
274
|
+
}
|
|
269
275
|
if (wantsThreads && (typeof body.maxConcurrentThreadsPerSession !== "number" || !Number.isInteger(body.maxConcurrentThreadsPerSession) || body.maxConcurrentThreadsPerSession < 1)) {
|
|
270
276
|
return jsonResponse({ error: "body.maxConcurrentThreadsPerSession must be an integer >= 1" }, 400);
|
|
271
277
|
}
|
|
@@ -333,6 +339,17 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
333
339
|
saveConfigPreservingClaudeCode(config);
|
|
334
340
|
warnings.push(`Multi-agent mode set to '${mode}'. Applies to new sessions.`);
|
|
335
341
|
}
|
|
342
|
+
if (wantsKeepNative) {
|
|
343
|
+
if (body.keepNativeChatGptOnV1 === true) config.keepNativeChatGptOnV1 = true;
|
|
344
|
+
else delete config.keepNativeChatGptOnV1;
|
|
345
|
+
saveConfigPreservingClaudeCode(config);
|
|
346
|
+
const effectiveMode = mode ?? config.multiAgentMode ?? "default";
|
|
347
|
+
warnings.push(body.keepNativeChatGptOnV1 === true
|
|
348
|
+
? (effectiveMode === "v2"
|
|
349
|
+
? "ChatGPT-native models stay on v1 while other models use v2. Applies to new sessions."
|
|
350
|
+
: "keepNativeChatGptOnV1 is stored but inactive until multi-agent mode is v2. Applies to new sessions.")
|
|
351
|
+
: "ChatGPT-native models follow the selected v1/v2/base surface. Applies to new sessions.");
|
|
352
|
+
}
|
|
336
353
|
// New-key scalar writes: each writer is individually atomic, so apply them in
|
|
337
354
|
// sequence after the transition. A failure here is a persistence failure (the
|
|
338
355
|
// writers' ok:false result or a throw from the underlying atomic write helper),
|
|
@@ -373,6 +390,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
373
390
|
agentsMaxThreadsConflict: enabled && hasAgentsMaxThreads(),
|
|
374
391
|
maxConcurrentThreadsPerSession: getLogicalMaxThreads(),
|
|
375
392
|
multiAgentMode: config.multiAgentMode ?? "default",
|
|
393
|
+
keepNativeChatGptOnV1: config.keepNativeChatGptOnV1 === true,
|
|
376
394
|
agentsEnabled: getAgentsEnabled(),
|
|
377
395
|
agentsMaxDepth: getAgentsMaxDepth(),
|
|
378
396
|
subagentDeveloperInstructions: getSubagentDeveloperInstructions(),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { OcxConfig } from "../../types";
|
|
2
2
|
import type { NativeProfileApiDeps } from "../../codex/native-profile-api";
|
|
3
|
+
import type { CodexLogGuardProtectionDeps } from "../../codex/log-guard/protection";
|
|
4
|
+
import type { CodexLogGuardMaintenanceDeps } from "../../codex/log-guard/maintenance";
|
|
3
5
|
import type { StartupHealth } from "../../codex/autostart-health";
|
|
4
6
|
import type { StartupInstallAction } from "../startup-action-control";
|
|
5
7
|
import type { ManagementPrincipal } from "../management-auth";
|
|
@@ -70,6 +72,19 @@ export interface ManagementApiDeps {
|
|
|
70
72
|
performRestart: typeof performCodexRestart;
|
|
71
73
|
};
|
|
72
74
|
nativeProfileApi?: NativeProfileApiDeps;
|
|
75
|
+
/**
|
|
76
|
+
* Log Guard mutation seam. Production leaves this unset and therefore uses the
|
|
77
|
+
* owner-verified process enumerator, trusted L namespace and real config store.
|
|
78
|
+
* Route tests inject all three so they cannot depend on local Codex processes
|
|
79
|
+
* or create lock/config state outside the fixture.
|
|
80
|
+
*/
|
|
81
|
+
codexLogGuardProtectionDeps?: CodexLogGuardProtectionDeps;
|
|
82
|
+
/**
|
|
83
|
+
* Log Guard maintenance seam. Production reuses the same fail-closed process
|
|
84
|
+
* enumerator and L namespace as Protect; route tests keep all maintenance
|
|
85
|
+
* state inside their temporary Codex home.
|
|
86
|
+
*/
|
|
87
|
+
codexLogGuardMaintenanceDeps?: CodexLogGuardMaintenanceDeps;
|
|
73
88
|
}
|
|
74
89
|
|
|
75
90
|
|
|
@@ -97,7 +97,8 @@ import { providerDestinationResolvedError } from "../../lib/destination-policy";
|
|
|
97
97
|
import { enrichProviderFromCatalog, listKeyLoginProviders } from "../../oauth/key-providers";
|
|
98
98
|
import { deriveProviderPresets } from "../../providers/derive";
|
|
99
99
|
import { providerCodexAccountMode } from "../../providers/registry";
|
|
100
|
-
import { routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
100
|
+
import { encodedModelIdCollides, routedSlug, slugEquals } from "../../providers/slug-codec";
|
|
101
|
+
import { knownModelIdsForProvider } from "../../router";
|
|
101
102
|
import { COMBO_NAMESPACE, comboDisabledModelSelectors, comboModelId, preservesPhysicalComboProvider } from "../../combos";
|
|
102
103
|
import { clearProviderQuotaCache, fetchProviderQuotaReports } from "../../providers/quota";
|
|
103
104
|
import { isCanonicalOpenAiForwardProvider } from "../../providers/openai-tiers";
|
|
@@ -376,7 +377,6 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
376
377
|
const provider = typeof body.provider === "string" ? body.provider.trim() : "";
|
|
377
378
|
const modelId = typeof body.modelId === "string" ? body.modelId.trim() : "";
|
|
378
379
|
if (!provider || !modelId) return jsonResponse({ error: "provider and modelId are required" }, 400);
|
|
379
|
-
if (modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
|
|
380
380
|
if (!isValidProviderName(provider)) return jsonResponse({ error: "invalid provider name" }, 400);
|
|
381
381
|
if (!hasOwnProvider(config.providers, provider)) return jsonResponse({ error: "provider not configured" }, 404);
|
|
382
382
|
const displayName = typeof body.displayName === "string" && body.displayName.trim() ? body.displayName.trim() : undefined;
|
|
@@ -394,6 +394,10 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
394
394
|
if (existing.some(cm => routedSlug(cm.provider, cm.modelId) === newSlug)) {
|
|
395
395
|
return jsonResponse({ error: "duplicate model" }, 409);
|
|
396
396
|
}
|
|
397
|
+
const known = knownModelIdsForProvider(provider, config.providers[provider], config);
|
|
398
|
+
if (encodedModelIdCollides(modelId, known)) {
|
|
399
|
+
return jsonResponse({ error: "ambiguous model id" }, 409);
|
|
400
|
+
}
|
|
397
401
|
const entry: OcxCustomModel = {
|
|
398
402
|
id: randomUUID(),
|
|
399
403
|
provider,
|
|
@@ -422,7 +426,6 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
422
426
|
if (idx === -1) return jsonResponse({ error: "not found" }, 404);
|
|
423
427
|
const cm = { ...list[idx] };
|
|
424
428
|
if (typeof body.modelId === "string" && body.modelId.trim()) {
|
|
425
|
-
if (body.modelId.includes("/")) return jsonResponse({ error: "modelId must not contain /" }, 400);
|
|
426
429
|
cm.modelId = body.modelId.trim();
|
|
427
430
|
}
|
|
428
431
|
if (body.displayName !== undefined) {
|
|
@@ -469,6 +472,12 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
469
472
|
if (list.some((other, i) => i !== idx && routedSlug(other.provider, other.modelId) === updatedSlug)) {
|
|
470
473
|
return jsonResponse({ error: "duplicate model" }, 409);
|
|
471
474
|
}
|
|
475
|
+
const known = knownModelIdsForProvider(cm.provider, config.providers[cm.provider], {
|
|
476
|
+
customModels: list.filter((_, i) => i !== idx),
|
|
477
|
+
});
|
|
478
|
+
if (encodedModelIdCollides(cm.modelId, known)) {
|
|
479
|
+
return jsonResponse({ error: "ambiguous model id" }, 409);
|
|
480
|
+
}
|
|
472
481
|
list[idx] = cm;
|
|
473
482
|
config.customModels = list;
|
|
474
483
|
persistConfig(config);
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { resolveCodexHomeDir } from "../../codex/home";
|
|
2
|
+
import {
|
|
3
|
+
compactCodexLogs,
|
|
4
|
+
type CodexLogGuardCompactionResult,
|
|
5
|
+
} from "../../codex/log-guard/maintenance";
|
|
6
|
+
import {
|
|
7
|
+
getCodexLogGuardProtectionStatus,
|
|
8
|
+
protectCodexLogs,
|
|
9
|
+
repairCodexLogGuardProtection,
|
|
10
|
+
unprotectCodexLogs,
|
|
11
|
+
type CodexLogGuardMutationResult,
|
|
12
|
+
type CodexLogGuardStatus,
|
|
13
|
+
} from "../../codex/log-guard/protection";
|
|
14
|
+
import { scanStorage } from "../../storage/scanner";
|
|
15
|
+
import { jsonResponse } from "../auth-cors";
|
|
16
|
+
import {
|
|
17
|
+
managementBodyTooLargeResponse,
|
|
18
|
+
readManagementJsonBody,
|
|
19
|
+
} from "./body";
|
|
20
|
+
import type { ManagementContext } from "./context";
|
|
21
|
+
|
|
22
|
+
const INSPECTION_FAILED_MESSAGE = "Codex log inspection failed";
|
|
23
|
+
|
|
24
|
+
function inspectionUnavailable(report: CodexLogGuardStatus): boolean {
|
|
25
|
+
return report.schema.state === "unavailable";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function mutationStatus(result: CodexLogGuardMutationResult): number {
|
|
29
|
+
if (result.ok) return 200;
|
|
30
|
+
switch (result.error) {
|
|
31
|
+
case "process_enumeration_failed":
|
|
32
|
+
return 503;
|
|
33
|
+
case "codex_running":
|
|
34
|
+
case "busy":
|
|
35
|
+
case "unsupported_schema":
|
|
36
|
+
case "trigger_collision":
|
|
37
|
+
case "unsafe_path":
|
|
38
|
+
return 409;
|
|
39
|
+
case "database_error":
|
|
40
|
+
case "config_write_failed":
|
|
41
|
+
return 500;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function compactStatus(result: CodexLogGuardCompactionResult): number {
|
|
46
|
+
if (result.ok) return 200;
|
|
47
|
+
switch (result.error) {
|
|
48
|
+
case "process_enumeration_failed":
|
|
49
|
+
return 503;
|
|
50
|
+
case "codex_running":
|
|
51
|
+
case "busy":
|
|
52
|
+
case "unsupported_schema":
|
|
53
|
+
case "auto_vacuum_not_incremental":
|
|
54
|
+
case "unsafe_path":
|
|
55
|
+
case "integrity_check_failed":
|
|
56
|
+
return 409;
|
|
57
|
+
case "database_error":
|
|
58
|
+
return 500;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mutationResponse(
|
|
63
|
+
result: CodexLogGuardMutationResult,
|
|
64
|
+
ctx: ManagementContext,
|
|
65
|
+
): Response {
|
|
66
|
+
return result.ok
|
|
67
|
+
? jsonResponse(result.status, 200, ctx.req, ctx.config)
|
|
68
|
+
: jsonResponse({ error: result.error }, mutationStatus(result), ctx.req, ctx.config);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function compactResponse(
|
|
72
|
+
result: CodexLogGuardCompactionResult,
|
|
73
|
+
ctx: ManagementContext,
|
|
74
|
+
): Response {
|
|
75
|
+
if (result.ok) {
|
|
76
|
+
return jsonResponse({ report: result.report }, 200, ctx.req, ctx.config);
|
|
77
|
+
}
|
|
78
|
+
return jsonResponse(
|
|
79
|
+
result.error === "integrity_check_failed"
|
|
80
|
+
? { error: result.error, phase: result.phase }
|
|
81
|
+
: { error: result.error },
|
|
82
|
+
compactStatus(result),
|
|
83
|
+
ctx.req,
|
|
84
|
+
ctx.config,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function readProtectMode(ctx: ManagementContext): Promise<"compat" | "quiet" | Response> {
|
|
89
|
+
let body: unknown;
|
|
90
|
+
try {
|
|
91
|
+
body = await readManagementJsonBody(ctx.req);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const tooLarge = managementBodyTooLargeResponse(error, ctx.req, ctx.config);
|
|
94
|
+
if (tooLarge) return tooLarge;
|
|
95
|
+
return jsonResponse({ error: "invalid_request" }, 400, ctx.req, ctx.config);
|
|
96
|
+
}
|
|
97
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
98
|
+
return jsonResponse({ error: "invalid_request" }, 400, ctx.req, ctx.config);
|
|
99
|
+
}
|
|
100
|
+
const mode = (body as Record<string, unknown>).mode;
|
|
101
|
+
if (mode !== "compat" && mode !== "quiet") {
|
|
102
|
+
return jsonResponse({ error: "invalid_mode" }, 400, ctx.req, ctx.config);
|
|
103
|
+
}
|
|
104
|
+
return mode;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Codex Log Guard diagnostics plus explicit protection and maintenance mutations. */
|
|
108
|
+
export async function handleStorageLogGuardRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
109
|
+
const { req, url, config, deps } = ctx;
|
|
110
|
+
const protectionDeps = deps.codexLogGuardProtectionDeps;
|
|
111
|
+
|
|
112
|
+
if (url.pathname === "/api/storage/codex-logs") {
|
|
113
|
+
if (req.method !== "GET") return null;
|
|
114
|
+
try {
|
|
115
|
+
const report = getCodexLogGuardProtectionStatus(protectionDeps);
|
|
116
|
+
if (inspectionUnavailable(report)) {
|
|
117
|
+
return jsonResponse({ error: "inspect_failed", message: INSPECTION_FAILED_MESSAGE }, 500, req, config);
|
|
118
|
+
}
|
|
119
|
+
return jsonResponse(report, 200, req, config);
|
|
120
|
+
} catch {
|
|
121
|
+
return jsonResponse({
|
|
122
|
+
error: "inspect_failed",
|
|
123
|
+
message: INSPECTION_FAILED_MESSAGE,
|
|
124
|
+
}, 500, req, config);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (url.pathname === "/api/storage/codex-logs/protect") {
|
|
129
|
+
if (req.method !== "POST") return null;
|
|
130
|
+
const mode = await readProtectMode(ctx);
|
|
131
|
+
if (mode instanceof Response) return mode;
|
|
132
|
+
return mutationResponse(protectCodexLogs(mode, protectionDeps), ctx);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (url.pathname === "/api/storage/codex-logs/unprotect") {
|
|
136
|
+
if (req.method !== "POST") return null;
|
|
137
|
+
return mutationResponse(unprotectCodexLogs(protectionDeps), ctx);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (url.pathname === "/api/storage/codex-logs/repair") {
|
|
141
|
+
if (req.method !== "POST") return null;
|
|
142
|
+
return mutationResponse(repairCodexLogGuardProtection(protectionDeps), ctx);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (url.pathname === "/api/storage/codex-logs/compact") {
|
|
146
|
+
if (req.method !== "POST") return null;
|
|
147
|
+
return compactResponse(compactCodexLogs(deps.codexLogGuardMaintenanceDeps), ctx);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (url.pathname !== "/api/storage" || req.method !== "GET") return null;
|
|
151
|
+
|
|
152
|
+
// Keep the existing CODEX_HOME scan as the primary storage contract. The Log Guard
|
|
153
|
+
// report is attached separately so an external sqlite_home is visible without being
|
|
154
|
+
// silently folded into CODEX_HOME totals.
|
|
155
|
+
let storage;
|
|
156
|
+
try {
|
|
157
|
+
storage = scanStorage();
|
|
158
|
+
} catch {
|
|
159
|
+
const fallback = {
|
|
160
|
+
codexHome: resolveCodexHomeDir(),
|
|
161
|
+
generatedAt: Date.now(),
|
|
162
|
+
total: { bytes: 0, fileCount: 0 },
|
|
163
|
+
buckets: [],
|
|
164
|
+
error: "scan_failed",
|
|
165
|
+
};
|
|
166
|
+
try {
|
|
167
|
+
const report = getCodexLogGuardProtectionStatus(protectionDeps);
|
|
168
|
+
return inspectionUnavailable(report)
|
|
169
|
+
? jsonResponse({ ...fallback, codexLogs: null, codexLogsError: "inspect_failed" }, 200, req, config)
|
|
170
|
+
: jsonResponse({ ...fallback, codexLogs: report }, 200, req, config);
|
|
171
|
+
} catch {
|
|
172
|
+
return jsonResponse({ ...fallback, codexLogs: null, codexLogsError: "inspect_failed" }, 200, req, config);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const report = getCodexLogGuardProtectionStatus(protectionDeps);
|
|
178
|
+
return inspectionUnavailable(report)
|
|
179
|
+
? jsonResponse({ ...storage, codexLogs: null, codexLogsError: "inspect_failed" }, 200, req, config)
|
|
180
|
+
: jsonResponse({ ...storage, codexLogs: report }, 200, req, config);
|
|
181
|
+
} catch {
|
|
182
|
+
// Log Guard inspection is auxiliary to the existing Storage page. A config/path
|
|
183
|
+
// resolution failure must not take the legacy read-only storage report down with it.
|
|
184
|
+
return jsonResponse({ ...storage, codexLogs: null, codexLogsError: "inspect_failed" }, 200, req, config);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -59,6 +59,7 @@ import { applySystemEnvToggle } from "./system-env";
|
|
|
59
59
|
import type { ManagementApiDeps } from "./management/context";
|
|
60
60
|
import { handleConfigRoutes } from "./management/config-routes";
|
|
61
61
|
import { handleLogsUsageRoutes } from "./management/logs-usage-routes";
|
|
62
|
+
import { handleStorageLogGuardRoutes } from "./management/storage-log-guard-routes";
|
|
62
63
|
import { handleRequestHistoryRoutes } from "./management/request-history-routes";
|
|
63
64
|
import { handleRoutingAnalyticsRoutes } from "./management/routing-analytics-routes";
|
|
64
65
|
import { handleProviderRoutes } from "./management/provider-routes";
|
|
@@ -209,6 +210,7 @@ export async function handleManagementAPI(
|
|
|
209
210
|
let routed: Response | null;
|
|
210
211
|
try {
|
|
211
212
|
routed = (await handleConfigRoutes(ctx))
|
|
213
|
+
?? (await handleStorageLogGuardRoutes(ctx))
|
|
212
214
|
?? (await handleLogsUsageRoutes(ctx))
|
|
213
215
|
?? (await handleRequestHistoryRoutes(ctx))
|
|
214
216
|
?? (await handleRoutingAnalyticsRoutes(ctx))
|
|
@@ -300,4 +302,4 @@ export async function handleManagementAPI(
|
|
|
300
302
|
}
|
|
301
303
|
|
|
302
304
|
|
|
303
|
-
export { buildClaudeDesktopState, fetchAllModels } from "./management/shared";
|
|
305
|
+
export { buildClaudeDesktopState, fetchAllModels } from "./management/shared";
|
|
@@ -1080,6 +1080,19 @@ async function applyFinalRouteRequestNormalization(args: {
|
|
|
1080
1080
|
}
|
|
1081
1081
|
}
|
|
1082
1082
|
|
|
1083
|
+
// Generic Responses clients (e.g. AI-SDK apps) omit `store`, but the canonical
|
|
1084
|
+
// forward Codex backend rejects a native request without an explicit store:false.
|
|
1085
|
+
// Default it only there — every other Responses upstream (key-auth providers and
|
|
1086
|
+
// custom forward gateways) intentionally keeps the omitted-store server-side
|
|
1087
|
+
// default for previous_response_id reuse — and never override an explicit value.
|
|
1088
|
+
if (
|
|
1089
|
+
isCanonicalOpenAiForwardProvider(route.provider)
|
|
1090
|
+
&& parsed._rawBody && typeof parsed._rawBody === "object"
|
|
1091
|
+
&& (parsed._rawBody as Record<string, unknown>).store === undefined
|
|
1092
|
+
) {
|
|
1093
|
+
(parsed._rawBody as Record<string, unknown>).store = false;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1083
1096
|
// Final selected model before virtual wire-model rewriting (Pro aliases).
|
|
1084
1097
|
const finalSelectedModelId = route.modelId;
|
|
1085
1098
|
|
package/src/server/system-env.ts
CHANGED
|
@@ -335,7 +335,7 @@ export async function injectSystemEnv(port: number, config: OcxConfig): Promise<
|
|
|
335
335
|
// without a token — keep it in sync with this proxy's /v1/models. Best-effort.
|
|
336
336
|
try {
|
|
337
337
|
const { refreshGatewayModelCacheFromProxy } = await import("../claude/gateway-cache");
|
|
338
|
-
await refreshGatewayModelCacheFromProxy(port);
|
|
338
|
+
await refreshGatewayModelCacheFromProxy(port, { admissionConfig: config });
|
|
339
339
|
} catch { /* best-effort */ }
|
|
340
340
|
|
|
341
341
|
// Roster agent definitions (devlog 070): same launch-time sync for plain `claude`.
|
package/src/types.ts
CHANGED
|
@@ -180,10 +180,27 @@ export interface OcxToolCall {
|
|
|
180
180
|
arguments: Record<string, unknown>;
|
|
181
181
|
customWireName?: string;
|
|
182
182
|
thoughtSignature?: string;
|
|
183
|
+
/**
|
|
184
|
+
* Provider-issued opaque metadata that must survive the whole round trip unchanged
|
|
185
|
+
* (issue #1735). A signed Gemini part is only valid when its signature comes back on the
|
|
186
|
+
* SAME part it was issued for, so this travels with the individual tool call rather than
|
|
187
|
+
* being matched by name/arguments after the fact.
|
|
188
|
+
*/
|
|
189
|
+
providerMetadata?: OcxProviderOpaqueToolCallMetadata;
|
|
183
190
|
/** MCP namespace (e.g. "mcp__context7") when this call targets a namespaced tool. */
|
|
184
191
|
namespace?: string;
|
|
185
192
|
}
|
|
186
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Opaque, provider-scoped tool-call metadata. Values are never parsed, merged, re-encoded, or
|
|
196
|
+
* synthesized — they are carried verbatim or not at all.
|
|
197
|
+
*/
|
|
198
|
+
export interface OcxProviderOpaqueToolCallMetadata {
|
|
199
|
+
google?: {
|
|
200
|
+
thoughtSignature?: string;
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
187
204
|
export type OcxAssistantContentPart = OcxTextContent | OcxThinkingContent | OcxToolCall;
|
|
188
205
|
|
|
189
206
|
export interface OcxTool {
|
|
@@ -328,7 +345,7 @@ export type AdapterEvent =
|
|
|
328
345
|
// Never rendered — it only rides the reasoning item's envelope so the next request can replay it.
|
|
329
346
|
| { type: "kiro_redacted_reasoning"; data: string }
|
|
330
347
|
| { type: "reasoning_raw_delta"; text: string }
|
|
331
|
-
| { type: "tool_call_start"; id: string; name: string }
|
|
348
|
+
| { type: "tool_call_start"; id: string; name: string; providerMetadata?: OcxProviderOpaqueToolCallMetadata }
|
|
332
349
|
| { type: "tool_call_delta"; arguments: string }
|
|
333
350
|
| { type: "tool_call_end" }
|
|
334
351
|
/** Internal boundary between a guarded first pass and its one-shot continuation. */
|
|
@@ -579,7 +596,7 @@ export interface OcxCustomModel {
|
|
|
579
596
|
id: string;
|
|
580
597
|
/** 프로바이더 키 (기존 providers[name]) */
|
|
581
598
|
provider: string;
|
|
582
|
-
/**
|
|
599
|
+
/** Native provider model id; slashes are allowed and encoded for Codex as provider/<hyphenated-id>. */
|
|
583
600
|
modelId: string;
|
|
584
601
|
/** 인간 가독 표시명 (선택, 슬래시 불가) */
|
|
585
602
|
displayName?: string;
|
|
@@ -808,6 +825,11 @@ export interface OcxConfig {
|
|
|
808
825
|
* - "v2": force ALL models to v2 surface (override upstream pins)
|
|
809
826
|
*/
|
|
810
827
|
multiAgentMode?: "v1" | "default" | "v2";
|
|
828
|
+
/**
|
|
829
|
+
* When `multiAgentMode` is `"v2"`, keep ChatGPT-native catalog rows on v1.
|
|
830
|
+
* Routed parents get v2 tools; Sol/Terra can still spawn Grok/Claude (issue #92).
|
|
831
|
+
*/
|
|
832
|
+
keepNativeChatGptOnV1?: boolean;
|
|
811
833
|
/** Experimental, default-off ChatGPT recovery for encrypted V2 routed tasks. */
|
|
812
834
|
agentTaskRecovery?: {
|
|
813
835
|
enabled?: boolean;
|
package/src/web-search/loop.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AdapterRequest, IncomingMeta, ProviderAdapter } from "../adapters/base";
|
|
2
|
-
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
|
|
2
|
+
import type { AdapterEvent, OcxMessage, OcxParsedRequest, OcxProviderConfig, OcxProviderOpaqueToolCallMetadata, OcxThinkingContent, OcxUsage, RateLimitRetryPolicy } from "../types";
|
|
3
3
|
import { namespacedToolName, toolChoiceToolPredicate } from "../types";
|
|
4
|
+
import { cloneProviderOpaqueToolCallMetadata } from "../responses/provider-opaque-metadata";
|
|
4
5
|
import type { AttemptRecoveryKind } from "../usage/log";
|
|
5
6
|
import { bridgeToResponsesSSE } from "../bridge";
|
|
6
7
|
import { runWebSearch, type SidecarOutcome, type SidecarOutcomeRecorder, type SidecarSettings } from "./executor";
|
|
@@ -32,6 +33,11 @@ interface WebSearchCall {
|
|
|
32
33
|
// empty array means the model called the tool with neither `query` nor `queries` (handled as an
|
|
33
34
|
// empty-query placeholder).
|
|
34
35
|
queries: string[];
|
|
36
|
+
/**
|
|
37
|
+
* Provider-opaque metadata from the originating part (issue #1735). Stored PER CALL so a
|
|
38
|
+
* signature can never migrate to a different call when the model batches several.
|
|
39
|
+
*/
|
|
40
|
+
providerMetadata?: OcxProviderOpaqueToolCallMetadata;
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/**
|
|
@@ -69,7 +75,7 @@ export function scanEventsForWebSearch(events: AdapterEvent[]): {
|
|
|
69
75
|
const passthrough: AdapterEvent[] = [];
|
|
70
76
|
let hasRealToolCall = false;
|
|
71
77
|
let hasMalformedToolCall = false;
|
|
72
|
-
let pending: { name: string; id: string; argsBuf: string; closed: boolean; events: AdapterEvent[] } | null = null;
|
|
78
|
+
let pending: { name: string; id: string; argsBuf: string; closed: boolean; events: AdapterEvent[]; providerMetadata?: OcxProviderOpaqueToolCallMetadata } | null = null;
|
|
73
79
|
const isBlank = (value: string): boolean => value.trim().length === 0;
|
|
74
80
|
const flushPending = (): void => {
|
|
75
81
|
// A pending call that never saw tool_call_end is structurally malformed.
|
|
@@ -84,7 +90,7 @@ export function scanEventsForWebSearch(events: AdapterEvent[]): {
|
|
|
84
90
|
if (e.type === "tool_call_start") {
|
|
85
91
|
flushPending();
|
|
86
92
|
if (isBlank(e.id) || isBlank(e.name)) hasMalformedToolCall = true;
|
|
87
|
-
pending = { name: e.name, id: e.id, argsBuf: "", closed: false, events: [e] };
|
|
93
|
+
pending = { name: e.name, id: e.id, argsBuf: "", closed: false, events: [e], providerMetadata: e.providerMetadata };
|
|
88
94
|
} else if (e.type === "tool_call_delta") {
|
|
89
95
|
// Orphan delta (no open call) is malformed.
|
|
90
96
|
if (!pending) hasMalformedToolCall = true;
|
|
@@ -100,7 +106,7 @@ export function scanEventsForWebSearch(events: AdapterEvent[]): {
|
|
|
100
106
|
pending.events.push(e);
|
|
101
107
|
pending.closed = true;
|
|
102
108
|
if (pending.name === WEB_SEARCH_TOOL_NAME) {
|
|
103
|
-
calls.push({ id: pending.id, queries: parseQueries(pending.argsBuf) });
|
|
109
|
+
calls.push({ id: pending.id, queries: parseQueries(pending.argsBuf), providerMetadata: pending.providerMetadata });
|
|
104
110
|
} else {
|
|
105
111
|
passthrough.push(...pending.events);
|
|
106
112
|
if (!isBlank(pending.id) && !isBlank(pending.name)) hasRealToolCall = true;
|
|
@@ -678,7 +684,17 @@ export async function runWithWebSearch(deps: WebSearchLoopDeps): Promise<Respons
|
|
|
678
684
|
// Signed thinking must precede tool_use on replay (Anthropic extended thinking), and
|
|
679
685
|
// unsigned raw reasoning has to ride along for providers that require it back (#688).
|
|
680
686
|
...precedingThinking,
|
|
681
|
-
{
|
|
687
|
+
{
|
|
688
|
+
type: "toolCall" as const,
|
|
689
|
+
id: call.id,
|
|
690
|
+
name: WEB_SEARCH_TOOL_NAME,
|
|
691
|
+
arguments: callArgs,
|
|
692
|
+
// Re-attach the signature to the rebuilt call so a sidecar turn keeps Gemini
|
|
693
|
+
// reasoning continuity instead of relying on the same-process replay cache.
|
|
694
|
+
...(cloneProviderOpaqueToolCallMetadata(call.providerMetadata)
|
|
695
|
+
? { providerMetadata: cloneProviderOpaqueToolCallMetadata(call.providerMetadata) }
|
|
696
|
+
: {}),
|
|
697
|
+
},
|
|
682
698
|
],
|
|
683
699
|
timestamp: now,
|
|
684
700
|
});
|