Compare commits

..

2 Commits

Author SHA1 Message Date
benzntech
50082ce21a feat(providers): add Cookie Editor fast-path to web session credential guide
The 'How to get the session credential' instructions in the provider
add-connection modal only described the manual DevTools flow. Add a
fast-path step using the Cookie Editor extension (export as Cookie
header, select all numbered session-token chunks) and demote the
DevTools walkthrough to the manual alternative.

New i18n keys (webSessionGuideStep2Fast, webSessionGuideStep3Manual)
ship in en.json; other locales fall back to English until translated.
2026-08-09 02:22:57 -03:00
diegosouzapw
b1e7e50cc5 fix(deps): bump transitive deps for 6 Dependabot + remaining audit vulns on main
Same overrides as #9464 (ip-address, hono, fast-uri, socket.io-parser, undici)
applied directly to main. Also covers brace-expansion (scoped), js-yaml v4 copies,
and mermaid.

npm audit: 6→0 vulnerabilities.
Closes Dependabot #161-#166.
2026-08-09 02:22:56 -03:00
10 changed files with 61 additions and 227 deletions

26
.gitignore vendored
View File

@@ -72,6 +72,7 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
!.env.devin-bridge.example
!.env.homolog.example
# Provider API keys (never commit)
*.api-key
@@ -171,7 +172,6 @@ config/quality/test-impact-map.json
# GitNexus local index
.gitnexus
.worktrees
bin/omniroute.mjs
# Consistent with .dockerignore / .npmignore
.omc/
@@ -201,12 +201,17 @@ scripts/i18n/_pending-keys.json
.codegraph/
# Fumadocs generated source
.source/
/.source/
# Temporary local worktrees used to build unpublished npm tarballs
/.deploy-build-*/
# AI agent local settings and configs
.agents/
.antigravitycli/
.claude/
!tests/fixtures/devin-bridge/e2e-workspace/.claude/
!tests/fixtures/devin-bridge/e2e-workspace/.claude/**
# PR Reviews and local feedback files
pr_reviews*.json
@@ -233,7 +238,10 @@ omniroute.md
# mise configuration
mise.toml
_artifacts/ # release-green artifacts
# release-green artifacts (.gitignore has no inline comments — a trailing
# `# ...` becomes part of the pattern, so it must sit on its own line).
# Already covered by /_*/ above; kept explicit for discoverability.
_artifacts/
.claude-flow/
# ESLint file cache (npm run lint --cache / complexity ratchets)
@@ -243,6 +251,8 @@ _artifacts/ # release-green artifacts
# CI/local quality artifacts (eslint-results.json, quality-ratchet.md, etc.)
.artifacts/
# Isolated Devin bridge workspaces, evidence, and test databases
.sandbox/
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
.env.homolog
@@ -250,8 +260,12 @@ tests/homolog/.auth/
tests/homolog/ui/.auth/
homolog-report/
docker-compose.yml.bak
.playwright-cli/
# Playwright screenshot/log output. Today every artifact happens to land inside
# output/**/.playwright-cli/ (covered above), but anything written directly to
# output/ would otherwise show up as untracked.
/output/
# _tasks e um repo git SEPARADO (ver AGENTS.md). A linha _tasks/ (com barra) NAO
# ignora um SYMLINK chamado _tasks; /_tasks (ancorado) cobre arquivo/symlink/dir na raiz
# e impede que um git add -A recapture o symlink (incidente 2026-08-08).
# _tasks e um repo git SEPARADO (ver AGENTS.md). _tasks/ (com barra) NAO ignora um
# SYMLINK _tasks; /_tasks (ancorado) cobre symlink/dir na raiz (incidente 2026-08-08).
/_tasks

View File

@@ -408,13 +408,12 @@ export class CliproxyapiExecutor extends BaseExecutor {
input.log?.info?.("CPA", `CLIProxyAPI → ${url} (model: ${input.model}, shape: ${shape})`);
// _toolNameMap and _namespaceToolIdentityMap are in-memory channels to
// chatCore for response-side tool name restoration; never send them over
// the wire.
// _toolNameMap is an in-memory channel to chatCore for response-side
// tool name restoration; never send it over the wire.
const wireBody =
transformedBody && typeof transformedBody === "object"
? JSON.stringify(transformedBody, (key, value) =>
key === "_toolNameMap" || key === "_namespaceToolIdentityMap" ? undefined : value
key === "_toolNameMap" ? undefined : value
)
: JSON.stringify(transformedBody);

View File

@@ -207,6 +207,7 @@ import { stageTrace } from "./chatCore/stageTrace.ts";
import { attachCompressionUsageReceiptAfterAnalytics as attachCompressionUsageReceiptAfterAnalyticsFor } from "./chatCore/compressionUsageReceipt.ts";
import { prepareUpstreamBody } from "./chatCore/upstreamBody.ts";
import { getQuotaScopeLabelForProvider } from "../services/antigravityQuotaFamily.ts";
import {
getCallLogPipelineCaptureStreamChunks,
getCallLogPipelineMaxSizeBytes,
@@ -366,7 +367,9 @@ import {
isTpmExhausted,
isRpmExhausted,
} from "../services/geminiRateLimitTracker.ts";
import { isSmallEnoughForSemanticCache } from "../utils/estimateSize.ts";
/**
* Core chat handler - shared between SSE and Worker
* Returns { success, response, status, error } for caller to handle fallback
@@ -386,8 +389,10 @@ import { isSmallEnoughForSemanticCache } from "../utils/estimateSize.ts";
* @param {boolean} options.isCombo - Whether this request is from a combo
* @param {string} options.connectionId - Connection ID for settings lookup
*/
// extractSystemRoleMessages extracted to chatCore/claudeSystemRole.ts (#3501); re-exported above so
// existing importers (e.g. tests/unit/system-role-extraction.test.ts) keep resolving it from here.
export async function handleChatCore({
body,
modelInfo,
@@ -423,6 +428,7 @@ export async function handleChatCore({
/* fail open */
}
}
// Per-request model-routing metadata (first extracted slice of the request-setup phase).
const { apiFormat, customModelTargetFormat, requestedModel } = resolveChatCoreRequestSetup(
modelInfo,
@@ -436,6 +442,7 @@ export async function handleChatCore({
// (not Math.random) purely to satisfy CodeQL js/insecure-randomness — this id
// is a log-correlation token, not a security secret.
const traceId = globalThis.crypto.randomUUID().slice(0, 6);
// Emit request.started event for real-time dashboard
setImmediate(() => {
emit("request.started", {
@@ -519,6 +526,7 @@ export async function handleChatCore({
`long-running goal mode enabled: readinessMax=${agentGoalPolicy.readinessMaxTimeoutMs}ms streamRecovery=${agentGoalPolicy.streamRecoveryEnabled}`
);
}
let effectiveServiceTier: EffectiveServiceTier = "standard";
// Codex service-tier resolvers extracted to chatCore/serviceTier.ts (#3501); bind the per-request
// provider/credentials once and delegate so the existing call sites stay byte-identical.
@@ -547,6 +555,7 @@ export async function handleChatCore({
})
).catch(() => {});
};
// Key-health updater extracted to chatCore/keyHealth.ts (#3501); bind the per-request log once
// and delegate so the existing call sites stay byte-identical.
const recordKeyHealthStatus = (
@@ -554,9 +563,11 @@ export async function handleChatCore({
creds: Record<string, unknown> | null | undefined,
transport?: string
): void => recordKeyHealthStatusFor(status, creds, log, transport);
const persistCodexQuotaState = async (headers: Record<string, string> | null, status = 0) => {
const currentConnectionId = getCurrentConnectionId();
if (provider !== "codex" || !currentConnectionId || !headers) return;
try {
const existingProviderData =
credentials?.providerSpecificData && typeof credentials.providerSpecificData === "object"
@@ -571,23 +582,28 @@ export async function handleChatCore({
status,
});
if (!built) return;
if (built.exhaustionLog) {
log?.debug?.("CODEX", built.exhaustionLog);
}
// Invalidate the preflight cache for this connection so the next
// isModelAvailable check fetches fresh quota data.
if (status === 429) {
invalidateCodexQuotaCache(currentConnectionId);
}
await updateProviderConnection(currentConnectionId, {
providerSpecificData: built.nextProviderData,
});
credentials.providerSpecificData = built.nextProviderData;
} catch (err) {
const errMessage = err instanceof Error ? err.message : String(err);
log?.debug?.("CODEX", `Failed to persist codex quota state: ${errMessage}`);
}
};
// ── Phase 9.2: Idempotency check ──
// Resolve the idempotency key once here and reuse it at the Phase 9.2 save site below,
// rather than re-deriving it. (#3821-review LEDGER-6)
@@ -606,11 +622,13 @@ export async function handleChatCore({
if (idempotencyHit) {
return idempotencyHit;
}
// T07: Inject connectionId into credentials so executors can rotate API keys
// using providerSpecificData.extraApiKeys (API Key Round-Robin feature)
if (connectionId && credentials && !credentials.connectionId) {
credentials.connectionId = connectionId;
}
// Endpoint/format resolution extracted to chatCore/requestFormat.ts (#3501); pure derivation
// from the inbound request, destructured so every downstream use stays byte-identical.
const {
@@ -2246,19 +2264,8 @@ export async function handleChatCore({
// the latter is a Kiro/Claude passthrough alias channel with string values,
// while namespace identities carry `{namespace, name}` for the #7936 response
// seam. Extract first because Kiro merge may reuse `_toolNameMap` below.
//
// #9780 — prefer the dedicated channel: on a pivot the openai->claude/gemini
// step publishes its own alias map on `_toolNameMap`, so that property alone
// yields aliases here. The `_toolNameMap` read stays as the fallback for the
// non-pivot producers (executors/base.ts, cliproxyapi.ts, antigravity).
const namespaceIdentityMap = translatedBody._namespaceToolIdentityMap;
const requestToolIdentityMap =
namespaceIdentityMap instanceof Map
? namespaceIdentityMap
: translatedBody._toolNameMap instanceof Map
? translatedBody._toolNameMap
: null;
delete translatedBody._namespaceToolIdentityMap;
translatedBody._toolNameMap instanceof Map ? translatedBody._toolNameMap : null;
delete translatedBody._toolNameMap;
// Kiro: sanitize tool schemas before dispatch. Kiro returns 400 "Improperly
@@ -5018,6 +5025,7 @@ export async function handleChatCore({
}),
};
}
export function isTokenExpiringSoon(expiresAt, bufferMs = 5 * 60 * 1000) {
if (!expiresAt) return false;
const expiresAtMs = new Date(expiresAt).getTime();

View File

@@ -352,27 +352,7 @@ export function translateRequest(
...(hasProvider ? { _provider: provider } : {}),
}
: credentials;
// #9780 — carry the Responses namespace identity map across the pivot.
// Target translators return a brand-new object (buildKiroPayload et
// al.), dropping the non-enumerable property step 1 attached; the
// #7936 seam then gets null and namespace sub-tool calls come back
// flattened, which Codex rejects with `unsupported call: <name>`.
const identityMap = (result as Record<string, unknown>)._namespaceToolIdentityMap;
const translated = fromOpenAI(model, result, stream, translationCredentials);
if (
identityMap instanceof Map &&
translated &&
typeof translated === "object" &&
!((translated as Record<string, unknown>)._namespaceToolIdentityMap instanceof Map)
) {
Object.defineProperty(translated, "_namespaceToolIdentityMap", {
value: identityMap,
enumerable: false,
configurable: true,
writable: true,
});
}
result = translated;
result = fromOpenAI(model, result, stream, translationCredentials);
}
}
}

View File

@@ -752,19 +752,8 @@ export function openaiResponsesToOpenAIRequest(
delete result.prompt_cache_retention;
if (namespaceToolIdentityMap.size > 0) {
// chatCore extracts and deletes these transient side channels before dispatch.
// chatCore extracts and deletes this transient side channel before dispatch.
// Non-enumerability keeps internal request metadata off the upstream wire.
//
// Two properties on purpose (#9780): `_toolNameMap` is also the alias
// channel for openai-to-claude/gemini, which overwrite it on a pivot, so
// the identity map needs a name of its own. `_toolNameMap` stays populated
// for the existing consumers (executors/base.ts, cliproxyapi, antigravity).
Object.defineProperty(result, "_namespaceToolIdentityMap", {
value: namespaceToolIdentityMap,
enumerable: false,
configurable: true,
writable: true,
});
Object.defineProperty(result, "_toolNameMap", {
value: namespaceToolIdentityMap,
enumerable: false,

14
package-lock.json generated
View File

@@ -31,7 +31,7 @@
"clsx": "^2.1.1",
"commander": "^15.0.0",
"csv-stringify": "^6.7.0",
"dompurify": "^3.4.13",
"dompurify": "^3.4.12",
"express": "^5.2.1",
"fetch-socks": "^1.3.3",
"fflate": "^0.8.3",
@@ -17064,9 +17064,9 @@
"license": "MIT"
},
"node_modules/dompurify": {
"version": "3.4.13",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.13.tgz",
"integrity": "sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==",
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
"integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -27576,9 +27576,9 @@
"optional": true
},
"node_modules/nanoid": {
"version": "3.3.18",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"funding": [
{
"type": "github",

View File

@@ -256,7 +256,7 @@
"clsx": "^2.1.1",
"commander": "^15.0.0",
"csv-stringify": "^6.7.0",
"dompurify": "^3.4.13",
"dompurify": "^3.4.12",
"express": "^5.2.1",
"fetch-socks": "^1.3.3",
"fflate": "^0.8.3",
@@ -395,6 +395,7 @@
]
},
"overrides": {
"dompurify": "^3.4.12",
"fast-xml-parser": "^5.10.1",
"sharp": "^0.35.0",
"postcss": "^8.5.18",
@@ -453,10 +454,6 @@
},
"xmlbuilder2": {
"js-yaml": "^4.3.1"
},
"nanoid": "^3.3.17",
"monaco-editor": {
"dompurify": "^3.4.13"
}
}
}

View File

@@ -111,16 +111,16 @@ export default function WebSessionCredentialGuide({
<li>
{providerText(
t,
"webSessionGuideStep2",
"Open the browser developer tools and inspect a request made by the web app."
"webSessionGuideStep2Fast",
"Fast path: install the Cookie Editor extension (chromewebstore.google.com → Cookie Editor), open it on the {provider} tab, find {credential} (select all numbered chunks if split), and click Export → Copy with the export format set to “Cookie header”.",
{ provider: providerName, credential: requirement.credentialName }
)}
</li>
<li>
{providerText(
t,
"webSessionGuideStep3",
"Copy the required credential from the provider's own domain. For cookies, copy only the Cookie header value and omit Cookie:.",
{ credential: requirement.credentialName }
"webSessionGuideStep3Manual",
"Manual path: open the browser developer tools (F12 → Network), refresh the page, open an authenticated request, and copy the Cookie header value from Request Headers — omit the Cookie: prefix."
)}
</li>
<li>

View File

@@ -5999,7 +5999,9 @@
"webTokenRequiredCredential": "Required token: {credential}",
"webSessionGuideStep1": "Sign in to {provider} in your browser.",
"webSessionGuideStep2": "Open the browser developer tools and inspect a request made by the web app.",
"webSessionGuideStep2Fast": "Fast path: install the Cookie Editor extension (chromewebstore.google.com → Cookie Editor), open it on the {provider} tab, find {credential} (select all numbered chunks if split), and click Export → Copy with the export format set to “Cookie header”.",
"webSessionGuideStep3": "Copy the required credential from the provider's own domain. For cookies, copy only the Cookie header value and omit Cookie:.",
"webSessionGuideStep3Manual": "Manual path: open the browser developer tools (F12 → Network), refresh the page, open an authenticated request, and copy the Cookie header value from Request Headers — omit the Cookie: prefix.",
"webSessionGuideStep4": "Paste it here and check the connection. If it stops working, sign in again and replace it with a fresh value.",
"webSessionSecurityHint": "Treat this like a password: it may access your signed-in web account until it expires or is revoked.",
"webNoAuthGuideTitle": "No credential required",

View File

@@ -1,155 +0,0 @@
// #9780 — the Responses namespace identity map must survive the hub-and-spoke
// pivot in translator/index.ts. Step 1 flattens namespace sub-tools (#8295) and
// records `{namespace, name}`; step 2 returns a new object and used to drop it,
// leaving the #7936 seam with null and Codex rejecting `unsupported call`.
// A naive copy-through is not an option: openai-to-claude/gemini publish their
// own alias map on `_toolNameMap`, hence the dedicated channel asserted here.
import test from "node:test";
import assert from "node:assert/strict";
await import("../../open-sse/translator/bootstrap.ts");
const { translateRequest, initState } = await import("../../open-sse/translator/index.ts");
const { openaiToOpenAIResponsesResponse } = await import(
"../../open-sse/translator/response/openai-responses.ts"
);
const { FORMATS } = await import("../../open-sse/translator/formats.ts");
type NamespaceIdentity = { namespace: string; name: string };
const NAMESPACE_REQUEST = {
model: "any-model",
instructions: "coding agent",
input: [{ type: "message", role: "user", content: [{ type: "input_text", text: "go" }] }],
tools: [
{
type: "namespace",
name: "functions",
tools: [
{
name: "exec",
description: "Run a shell command",
parameters: {
type: "object",
properties: { cmd: { type: "string" } },
required: ["cmd"],
},
},
],
},
],
};
function pivot(targetFormat: string): Record<string, unknown> {
return translateRequest(
"openai-responses",
targetFormat,
"any-model",
structuredClone(NAMESPACE_REQUEST),
true,
null,
null,
null
) as Record<string, unknown>;
}
function identityOf(body: Record<string, unknown>) {
const map = body._namespaceToolIdentityMap;
assert.ok(map instanceof Map, "expected a _namespaceToolIdentityMap after the pivot");
return map as Map<string, NamespaceIdentity>;
}
test("#9780: namespace identity survives the openai-responses -> kiro pivot", () => {
const identity = identityOf(pivot("kiro"));
assert.equal(identity.size, 1);
assert.deepEqual(identity.get("functions__exec"), { namespace: "functions", name: "exec" });
});
test("#9780: namespace identity survives the openai-responses -> cursor pivot", () => {
const identity = identityOf(pivot("cursor"));
assert.deepEqual(identity.get("functions__exec"), { namespace: "functions", name: "exec" });
});
// Regression guard: these two appeared to "keep" a map before the fix, but it
// was the alias map.
for (const target of ["claude", "gemini"]) {
test(`#9780: ${target} pivot keeps its alias map AND the namespace identity`, () => {
const body = pivot(target);
const identity = identityOf(body);
assert.deepEqual(identity.get("functions__exec"), { namespace: "functions", name: "exec" });
// The alias channel must be untouched: string values, not identities.
const aliases = body._toolNameMap;
assert.ok(aliases instanceof Map, `${target} must still publish its alias map`);
for (const value of (aliases as Map<string, unknown>).values()) {
assert.equal(typeof value, "string", `${target} alias values must stay strings`);
}
});
}
// Same-format requests are never flattened, so an absent map is correct here.
test("#9780: same-format openai-responses request is not flattened at all", () => {
const body = pivot("openai-responses");
const tools = body.tools as Array<Record<string, unknown>>;
assert.equal(tools[0].type, "namespace");
assert.equal((tools[0].tools as Array<{ name: string }>)[0].name, "exec");
assert.equal(body._namespaceToolIdentityMap, undefined);
});
test("#9780: the identity channel is non-enumerable and never serializes", () => {
const body = pivot("kiro");
assert.ok(body._namespaceToolIdentityMap instanceof Map);
assert.equal(
Object.prototype.propertyIsEnumerable.call(body, "_namespaceToolIdentityMap"),
false
);
assert.equal("_namespaceToolIdentityMap" in JSON.parse(JSON.stringify(body)), false);
});
// End-to-end: request pivot + response seam, i.e. what the Codex adjudicator
// actually receives. Before the fix every target emitted `functions__exec` with
// no namespace, which is the reported `unsupported call`.
for (const target of ["kiro", "cursor", "claude", "gemini"]) {
test(`#9780: ${target} round-trip returns the declared name and its namespace`, () => {
const body = pivot(target);
const state = initState(FORMATS.OPENAI_RESPONSES) as Record<string, unknown>;
state.requestToolIdentityMap = body._namespaceToolIdentityMap;
// The upstream echoes the flattened wire name (#8295).
const events = openaiToOpenAIResponsesResponse(
{
id: "chatcmpl-9780",
model: "any-model",
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: "call_9780",
type: "function",
function: { name: "functions__exec", arguments: '{"cmd":"git status"}' },
},
],
},
finish_reason: "tool_calls",
},
],
usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 },
},
state
) as Array<{ event: string; data: { item?: NamespaceIdentity } }>;
const added = events.find((e) => e.event === "response.output_item.added")?.data.item;
assert.ok(added, "expected response.output_item.added");
assert.deepEqual(
{ name: added.name, namespace: added.namespace },
{ name: "exec", namespace: "functions" }
);
});
}