Skip to content

fix: close AP-ISS-0197 API-route OCR findings - #142

Open
khanhkit wants to merge 17 commits into
mainfrom
kit/KitDev25/session-6
Open

khanhkit wants to merge 17 commits into
mainfrom
kit/KitDev25/session-6

Conversation

@khanhkit

@khanhkit khanhkit commented Oct 15, 2026 •

Copy link
Copy Markdown
Owner

AP-ISS-0197

Terminally dispositions all 21 API-route security/lifecycle OCR findings against current accepted code, with minimal RED→GREEN fixes only for reproduced defects.

Terminal dispositions

  • 17 reproduced + fixed
  • 3 not reproduced / current-safe
  • 1 not applicable / false premise (4233363247: management-authenticated GLM reset-card route with no modeled tenant/owner column on provider connections)

The full per-finding evidence ledger is committed at docs/audits/ap-iss-0197-dispositions.md.

Main integration

Protected origin/main 166b4cbb52595005522d2aed5d47282a0942918e was merged before publication and re-fetched unchanged immediately before the current head was pushed. The sole overlap, open-sse/services/cache/embeddingClient.ts, preserves both AP-ISS-0195 SSRF/cooldown hardening and AP-ISS-0197 embedding-dimension propagation.

Exact-head verification

Current published PR head: e9611720ae09c0a534756c85c213c57e2e22c3fe.

Fresh local evidence on this exact head:

  • AP-ISS-0197 focused superset, now including the legacy files/batches ownership regression plus embedding SSRF overlap + chat route-shape guard: 110 pass / 0 fail
  • npm run test:security: 23 pass / 0 fail
  • GITHUB_BASE_REF=main npm run check:test-masking: PASS
  • npm run check:docs-all: PASS
  • npm run typecheck:core: PASS (RC=0)
  • npm run check:open-sse-typecheck: PASS (RC=0, openSseTypecheckErrors=0)
  • node scripts/check/check-api-typecheck.mjs: PASS (apiTypecheckErrors=276, all frozen baseline; 0 new)
  • npm run check:route-guard-membership: PASS
  • npm run check:public-creds: PASS
  • scoped ESLint over 38 AP-ISS-0197 changed TypeScript/JavaScript files with the repository suppression bank: PASS
  • git diff --check origin/main...HEAD: PASS
  • npm run check:secrets: environment SKIP because gitleaks is unavailable, per repository gate behavior

A bounded full npm test was attempted before main integration. At the ~30-minute boundary Node reported 10,973 tests / 6,012 pass / 0 fail / 4,959 cancelled / 2 skipped. The wrapper did not persist a terminal return-code marker; this is recorded as incomplete timeout evidence and is not claimed as a green full-suite result.

Hosted follow-up

Predecessor heads exposed four actionable CI findings; all were reproduced locally before repair:

  1. PR Test Policy / test-masking — batches-delete-completed-route-scope.test.ts had dropped two behavioral assertions while changing the superseded key-wins expectation to AP-ISS-0197 session authority. The replacement restores two meaningful file-deletion postcondition assertions; test-masking passes.
  2. API Route Typecheck — ApiKeyRequestScope.apiKeyMetadata was tied to the DB-only full metadata type while policy hydration supplies the policy metadata shape. The boundary now models the common id/scopes subset; API-route typecheck is back at frozen baseline with 0 new errors.
  3. Docs Sync (Strict) — the audit prose named an internal wrapper marker using env-var-like syntax, which strict fabricated-docs interpreted as a claimed environment variable. The wording now describes it generically; check:docs-all passes.
  4. Unit Tests shard 8 — files-batches-ownership-2jm2-m3hp.test.ts still encoded the superseded “API key wins over dashboard session” rule in three cases. Only those mixed-session expectations were aligned to AP-ISS-0197 instance-operator authority; key-only invalid/ownership guards remain unchanged. The file is 13/13 green and the expanded focused set is 110/110 green.

No terminal merge/close claim is made until all required hosted workflows for exact head e9611720ae09… are terminal green and current review threads are re-read.

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

🔍 OpenCodeReview found 6 issue(s) in this PR.

  • ✅ Successfully posted inline: 5 comment(s)
  • ⏭️ Skipped (overlap with history): 1 comment(s)

body: JSON.stringify({
model,
input: text,
...(config.embeddingDimension ? { dimensions: config.embeddingDimension } : {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
dimensions is only validated as positive, so fractional values (for example 1.5) reach provider APIs via dimensions; OpenAI-compatible endpoints expect an integer and will reject the probe. Require z.number().int().positive() in the route schema (and ideally validate at this boundary too).

Suggestion:

Suggested change
...(config.embeddingDimension ? { dimensions: config.embeddingDimension } : {}),
...(config.embeddingDimension !== undefined
? { dimensions: config.embeddingDimension }
: {}),

Comment on lines 48 to 50
"semanticCacheEmbeddingDimension",
"semanticCacheEmbeddingBaseUrl",
"semanticCacheEmbeddingApiKey",
"semanticCacheRedisUrl",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
Removing the key from CACHE_CONFIG_KEYS makes the dashboard load semApiKey as empty, but saveSemanticCache() always sends semanticCacheEmbeddingApiKey: semApiKey.trim() || null; any subsequent semantic-cache save therefore clears the persisted provider credential, breaking embeddings. Keep the secret out of GET while preserving it on partial updates, or distinguish “unchanged” from explicit clear in the client/API.

Suggestion:

Suggested change
"semanticCacheEmbeddingDimension",
"semanticCacheEmbeddingBaseUrl",
"semanticCacheEmbeddingApiKey",
"semanticCacheRedisUrl",
"semanticCacheEmbeddingDimension",
"semanticCacheEmbeddingBaseUrl",
"semanticCacheRedisUrl",

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed at 1794e1654b92a3ff79a0abe1916d7ae096e311a8: GET continues to redact semanticCacheEmbeddingApiKey, while a PUT carrying redacted null preserves the stored credential. Focused cache-config-route-8219 regression verifies GET never leaks the secret and subsequent redacted save does not clear it (6/6 suite PASS).

Comment on lines 144 to 148
if (requiresBootstrapTokenClaim && !consumeBootstrapToken(bootstrapCandidate)) {
return NextResponse.json({ error: "Local bootstrap required" }, { status: 403 });
}

await updateSettings(updates);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · medium
Token is consumed before updateSettings succeeds. A transient DB/write failure now burns the one-shot bootstrap credential, leaving the operator unable to retry and potentially requiring manual recovery. Consume only after a successful write, or add an atomic claim/commit mechanism that can release the claim on failure while preserving one-shot concurrency semantics.

Suggestion:

Suggested change
if (requiresBootstrapTokenClaim && !consumeBootstrapToken(bootstrapCandidate)) {
return NextResponse.json({ error: "Local bootstrap required" }, { status: 403 });
}
await updateSettings(updates);
await updateSettings(updates);
if (requiresBootstrapTokenClaim && !consumeBootstrapToken(bootstrapCandidate)) {
return NextResponse.json({ error: "Local bootstrap required" }, { status: 403 });
}

Comment on lines +78 to +83
if (dialect === "tei") {
return {
...common,
texts: input.documents.map(documentText),
return_text: returnDocuments,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
TEI's /rerank schema does not accept the Cohere-only model/top_n fields (it expects query, texts, and optionally return_text). Because common is spread into this dialect, the retry still receives unknown fields and strict TEI responds 422, so the fallback remains broken. Build a TEI-specific body without those fields; apply top_n locally after normalization.

Suggestion:

Suggested change
if (dialect === "tei") {
return {
...common,
texts: input.documents.map(documentText),
return_text: returnDocuments,
};
if (dialect === "tei") {
return {
query: input.query,
texts: input.documents.map(documentText),
return_text: returnDocuments,
};

Comment on lines +198 to 209
const selfHostedConfigured = Boolean(
process.env[SELF_HOSTED_CONFIG_ENV] || process.env[SELF_HOSTED_CONFIG_FILE_ENV]
);
if (selfHostedConfigured) {
const requestedModel = typeof parsedBody.model === "string" ? parsedBody.model : null;
const selfHostedPolicy = await enforceApiKeyPolicy(request, requestedModel);
if (selfHostedPolicy.rejection) {
return finishAdmission(selfHostedPolicy.rejection);
}
}

const selfHostedResponse = await handleSelfHostedCompletions(request, parsedBody);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · high
Self-hosted policy runs for every request whenever either config env var exists, but handleSelfHostedCompletions can still return null for requests that are not routed to self-hosted. Cloud requests therefore receive self-hosted model/endpoint/quota checks and can be denied incorrectly. Determine routing first (or make policy conditional on the actual self-hosted route).

Suggestion:

Suggested change
const selfHostedConfigured = Boolean(
process.env[SELF_HOSTED_CONFIG_ENV] || process.env[SELF_HOSTED_CONFIG_FILE_ENV]
);
if (selfHostedConfigured) {
const requestedModel = typeof parsedBody.model === "string" ? parsedBody.model : null;
const selfHostedPolicy = await enforceApiKeyPolicy(request, requestedModel);
if (selfHostedPolicy.rejection) {
return finishAdmission(selfHostedPolicy.rejection);
}
}
const selfHostedResponse = await handleSelfHostedCompletions(request, parsedBody);
const selfHostedResponse = await handleSelfHostedCompletions(request, parsedBody);
if (selfHostedResponse) {
const requestedModel = typeof parsedBody.model === "string" ? parsedBody.model : null;
const selfHostedPolicy = await enforceApiKeyPolicy(request, requestedModel);
if (selfHostedPolicy.rejection) {
return finishAdmission(selfHostedPolicy.rejection);
}
return finishAdmission(selfHostedResponse);
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revalidated against the actual self-hosted entry contract at new PR HEAD 1794e1654b92a3ff79a0abe1916d7ae096e311a8: handleSelfHostedCompletions returns null only when BOTH config env vars are absent; when configured, it always returns a response (including config failures). The route's selfHostedConfigured uses those same vars, so there is no cloud fallback on this configured branch to misapply policy to. Moving policy after dispatch, as suggested, would authorize after an upstream side effect and violate the original security requirement. Classified NOT_REPRODUCED/FALSE_PREMISE in the tracked disposition ledger. Independent review remains welcome.

Comment on lines +203 to +206
const selfHostedPolicy = await enforceApiKeyPolicy(request, requestedModel);
if (selfHostedPolicy.rejection) {
return finishAdmission(selfHostedPolicy.rejection);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · high
enforceApiKeyPolicy is inside the broad prompt-injection try/catch; any policy/backend exception is logged, then execution continues into handleChat. This converts the policy's documented fail-closed behavior into a fail-open path. Catch policy errors separately and return a sanitized 5xx response (while releasing admission).

Suggestion:

Suggested change
const selfHostedPolicy = await enforceApiKeyPolicy(request, requestedModel);
if (selfHostedPolicy.rejection) {
return finishAdmission(selfHostedPolicy.rejection);
}
let selfHostedPolicy;
try {
selfHostedPolicy = await enforceApiKeyPolicy(request, requestedModel);
} catch {
return finishAdmission(errorResponse(503, "API key policy unavailable"));
}
if (selfHostedPolicy.rejection) {
return finishAdmission(selfHostedPolicy.rejection);
}

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed at 1794e1654b92a3ff79a0abe1916d7ae096e311a8: enforceSelfHostedPolicyOrReject separately catches policy/backend exceptions, returning sanitized 503 before handleSelfHostedCompletions, so the outer injection catch cannot fail-open. Focused self-hosted test verifies thrown policy -> 503; restricted API-key test verifies zero upstream calls.

Comment thread src/app/api/v1/rerank/route.ts Outdated
Comment on lines +219 to +221
if (res.status === 400 || res.status === 422) {
res = await fetch(activeUrl, makeUpstreamInit("tei"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
This retries every 400/422, not only schema-validation responses as the comment claims. Invalid query/documents, unsupported model, or provider-specific semantic errors trigger a second inference request and replace the original diagnostic; this also adds avoidable request-wide amplification. Inspect the error body/content type (or use a provider-specific schema signal) before retrying.

Suggestion:

Suggested change
if (res.status === 400 || res.status === 422) {
res = await fetch(activeUrl, makeUpstreamInit("tei"));
}
if (res.status === 400 || res.status === 422) {
const errorText = await res.clone().text().catch(() => "");
const isSchemaError = /(?:unknown|extra|missing|required|field|documents|texts)/i.test(errorText);
if (isSchemaError) {
res = await fetch(activeUrl, makeUpstreamInit("tei"));
}
}

Comment on lines +95 to +98
const allParts = [
...(value.contents ?? []).flatMap((content) => content.parts ?? []),
...(value.systemInstruction?.parts ?? []),
];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · medium
z.object(...).catchall(z.unknown()) accepts system_instruction, but this refinement only traverses value.systemInstruction. A request with contents plus snake_case system instruction can carry inline media outside both per-part and 16 MiB request accounting. Either reject the unsupported alias or normalize/include it in validation and downstream conversion.

Suggestion:

Suggested change
const allParts = [
...(value.contents ?? []).flatMap((content) => content.parts ?? []),
...(value.systemInstruction?.parts ?? []),
];
const allParts = [
...(value.contents ?? []).flatMap((content) => content.parts ?? []),
...(value.systemInstruction?.parts ?? []),
];

@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

CI Coverage Report

  • Coverage job: skipped
  • PR test policy: success

Coverage artifact was not available for this run.

Copy link
Copy Markdown
Owner Author

KitDev25 / AP-ISS-0197 — BLOCKED_RECOVERY (read-only host verification)

This is GitHub PR discussion evidence only, NOT an authoritative KitControl handoff, lease extension, recovery, or board-status mutation.

  • Host time: 2026-10-20 18:15:42–18:16:05 ICT (UTC+7), obtained from TZ=Asia/Ho_Chi_Minh date on KitMCP host.
  • Canonical files: runtime/actors/KitDev25/actor.toml, runtime/claims/AgentProxy/AP-ISS-0197/claim.toml, runtime/leases/KitDev25.toml, runtime/status/KitDev25.json, and runtime/handoffs/KitDev25.json re-read directly.
  • Actor and claim: Gen15 / KitDev25 / AP-ISS-0197, preserved task_run tr_be7263bfcfaf4b8595cf7bbfa5f4e749.
  • Lease CLAIMED but EXPIRED at 2026-10-20 15:56:43+07:00; status STALE_CANDIDATE, execution_state=IDLE, no attributable active/running processes; no renewed execution authority.
  • Existing owned worktree /home/coder/MCP/.kitcow/workspaces/KitDev25/AgentProxy, branch kit/KitDev25/session-6, HEAD e9611720ae09c0a534756c85c213c57e2e22c3fe, 16 modified tracked files preserved without reset, stash, cleanup, source edit, stage, commit, or push. PR fix: close AP-ISS-0197 API-route OCR findings #142 remains open at that HEAD; PR review remediation is not committed.
  • Canonical handoff still CLAIMED and contains stale clean HEAD 08bbb79b... / no blocker record. Do not treat it as accurate for WIP.
  • Attempted exactly one supported official ./bin/kit recover KitDev25 --validated after stale/process preflight, RC=1, failed opening /home/coder/MCP/KitControl/runtime/locks/KitDev25.lock: OSError: [Errno 30] Read-only file system. Immediate actor/claim/lease readback remained Gen15 and same expired lease. General Coding-bash sees KitControl runtime mounted RO; privileged goalkeeper_lifecycle/kitcontrol_lifecycle actions are not exposed in the current client tool surface.
  • CI status at existing PR HEAD includes historical agentproxy/runner-claim/.../1/1=failure; no new tests executed under the expired lease; no claim of passing full CI or task completion.

BLOCKER: Machine-private KitControl lifecycle recovery + durable handoff/escalation writes are unavailable in this client; stale Gen15 authority cannot safely resume owned mutation. This PR note is a noncanonical support breadcrumb, not a substitute for KitControl control-plane persistence.

NEXT: GoalKeeper/Architect use a deployed approved privileged lifecycle client to validate takeover safety, perform recover --validated once, and read back actor/claim/lease/task_run/worktree. Restore a truthful task-local blocked/active handoff via canonical API, verify bounded Git-admin tool and hosted PR state, then resume focused review tests and process the preserved 16-file WIP for PR #142.

DO-NOT: No raw actor/claim/lease/handoff edits, fabricated heartbeat/ACTIVE, anonymous/root execution, blind re-recovery, WIP reset/stash/clean, or force-push. Preserve Data-is-Gold.

Preserve one-shot bootstrap retry semantics and strict embedding/rerank/Gemini boundaries; fail closed on self-hosted policy failure. Add direct-peer/auth rejection regressions and 8-finding review disposition evidence.

khanhkit commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner Author

KitDev25 Gen16 — BLOCKED_EXECUTION_CONTENTION (do not merge)

Current published PR #142 head: c35014d5990b93739d99c675f436f589f564342c, verified on GitHub. Two review-remediation commits were published non-force by this ChatGPT continuation through the officially gated stage/commit channel (1794e1654b92a3ff79a0abe1916d7ae096e311a8, c35014d5990b93739d99c675f436f589f564342c). The worktree was clean at final readback; all Data-is-Gold/provenance preserved.

CRITICAL NEW COORDINATION BLOCKER: KitControl existing OPEN escalation ESC-9a32f88e8bd8e55417b3f59c (proposed 2026-10-20 19:19:31 ICT) independently observed a second Gen16 KitDev25 ChatGPT continuation running tests while the present continuation was editing tests/unit/rerank-providers-route.test.ts at 19:15:24 and running typechecks at 19:17+. This continuation discovered that existing escalation after publishing the commits. Do not infer unique execution authority; there were overlapping ChatGPT execution lanes on the same canonical claim/worktree. No automatic rollback or force-push is authorized. Canonical KitControl handoff+task have now been set to BLOCKED via the supported machine-private lifecycle block action, preserving Gen16 claim and task-run tr_be7263bfcfaf4b8595cf7bbfa5f4e749.

Verified local tests/gates: AP-ISS-0197 cache/embedding/rerank 14/14 PASS, extended set 72/72 PASS, Codex/GLM/ownership/cache set 51/51 PASS and auth/chat/cache integration set 89/89 PASS; security 23/23 PASS; core and Open-SSE typechecks, route-guard, public-creds, Prettier, scoped ESLint with suppression bank, diff check PASS. These are independent host invocations, not hosted exact-head CI. At last check for c35014..., API Route Typecheck and Semgrep SUCCESS, while CI queued, CodeQL/DAST/OpenCodeReview in progress; full local suite not run.

DO NOT MERGE OR CLOSE. Next: GoalKeeper/KitArchitect must reconcile ESC-9a32..., verify one canonical execution lane and no competing writer, independently inspect both commits/provenance, then get exact-head CI/security review and accepted-main integration before terminal disposition. No source/Git mutation is authorized in this continuation while execution contention remains unresolved. This PR comment is support evidence only; authoritative KitControl BLOCKED handoff and escalation control-plane govern.

…ality

Preserve management auth's fail-closed behavior; model a trusted loopback socket peer in the old settings regression and document red-to-green verification.
Comment on lines 25 to +27
const isSessionAuth = await isDashboardSessionAuthenticated(request);
if (isSessionAuth) {
return {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · high
Session detection now returns before parsing the bearer key. A request carrying a valid dashboard cookie plus an invalid/disabled/malformed API key is accepted as session-authenticated, whereas the previous flow rejected the invalid presented key. This lets clients bypass API-key validation/policy by adding a dashboard cookie and can hide credential-confusion errors. Parse and reject a presented invalid key before taking the session-auth fallback (or explicitly define and enforce session precedence across all API routes).

Suggestion:

Suggested change
const isSessionAuth = await isDashboardSessionAuthenticated(request);
if (isSessionAuth) {
return {
const isSessionAuth = await isDashboardSessionAuthenticated(request);
const apiKey = extractApiKey(request);
if (apiKey) {
const apiKeyMetadata = await getApiKeyMetadata(apiKey);
const keyState = apiKeyMetadata && (await validateApiKey(apiKey)) ? "valid" : "invalid";
if (keyState === "invalid") {
return {
apiKey,
apiKeyId: null,
apiKeyMetadata: null,
keyState,
rejection: unauthorized("Invalid API key"),
isSessionAuth,
};
}
}
if (isSessionAuth) {
return {

const systemText =
geminiBody.systemInstruction.parts?.map((p) => p.text ?? "").join("\n") || "";
// Convert either Gemini JS camelCase or REST snake_case system instruction.
const systemInstruction = geminiBody.systemInstruction ?? geminiBody.system_instruction;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

other · medium
When both accepted fields are present, ?? chooses systemInstruction and drops system_instruction without warning. Reject the ambiguous request or define a merge policy to avoid silently changing the prompt.

Suggestion:

Suggested change
const systemInstruction = geminiBody.systemInstruction ?? geminiBody.system_instruction;
if (geminiBody.systemInstruction && geminiBody.system_instruction) {
throw new Error("Specify only one system instruction field");
}
const systemInstruction = geminiBody.systemInstruction ?? geminiBody.system_instruction;

Comment on lines +27 to +32
for (const key of ["inlineData", "inline_data"]) {
const inline = record[key];
if (!inline || typeof inline !== "object" || Array.isArray(inline)) continue;
const data = (inline as { data?: unknown }).data;
if (typeof data === "string") payloads.push(data);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · low
When a part contains both spellings, validation totals both payloads, but conversion chooses only one via inlineData ?? inline_data. This makes request limits differ from actual forwarded content. Reject both keys together or use a consistent merge policy.

Suggestion:

Suggested change
for (const key of ["inlineData", "inline_data"]) {
const inline = record[key];
if (!inline || typeof inline !== "object" || Array.isArray(inline)) continue;
const data = (inline as { data?: unknown }).data;
if (typeof data === "string") payloads.push(data);
}
if (record.inlineData !== undefined && record.inline_data !== undefined) return [];

Comment on lines +46 to +50
for (const data of inlineDataPayloads(part)) {
if (
data.length > MAX_EMBEDDING_INLINE_ITEM_BASE64_LENGTH ||
decodedBase64Bytes(data) > MAX_EMBEDDING_INLINE_ITEM_BYTES
) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · medium
These size checks only estimate decoded length; unlike the embedding schema, they never verify base64 syntax. Malformed inline media can pass validation and be forwarded. Reuse a shared base64 validator and reject invalid data.

Suggestion:

Suggested change
for (const data of inlineDataPayloads(part)) {
if (
data.length > MAX_EMBEDDING_INLINE_ITEM_BASE64_LENGTH ||
decodedBase64Bytes(data) > MAX_EMBEDDING_INLINE_ITEM_BYTES
) {
for (const data of inlineDataPayloads(part)) {
if (
!isValidBase64(data) ||
data.length > MAX_EMBEDDING_INLINE_ITEM_BASE64_LENGTH ||
decodedBase64Bytes(data) > MAX_EMBEDDING_INLINE_ITEM_BYTES
) {

Comment on lines +26 to +35
if (isSessionAuth) {
return {
apiKey: null,
apiKeyId: null,
apiKeyMetadata: null,
keyState: "none",
rejection: null,
isSessionAuth: true,
};
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security · medium
Session auth returns before evaluating a supplied API key, so mixed-credential requests bypass that key’s ownership and scope restrictions and gain instance-wide access. Reject mixed credentials or evaluate the API key consistently.

Suggestion:

Suggested change
if (isSessionAuth) {
return {
apiKey: null,
apiKeyId: null,
apiKeyMetadata: null,
keyState: "none",
rejection: null,
isSessionAuth: true,
};
}
if (isSessionAuth && extractApiKey(request)) {
return {
apiKey: null,
apiKeyId: null,
apiKeyMetadata: null,
keyState: "invalid",
rejection: unauthorized("Conflicting credentials"),
isSessionAuth: true,
};
}
if (isSessionAuth) {
return {
apiKey: null,
apiKeyId: null,
apiKeyMetadata: null,
keyState: "none",
rejection: null,
isSessionAuth: true,
};
}

Comment on lines +230 to +232
if (await isLocalRerankSchemaValidationResponse(res)) {
res = await fetch(activeUrl, makeUpstreamInit("tei"));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance · medium
This broad match treats any 400/422 containing validation error or field required as a dialect mismatch. Invalid client/provider input (for example an unsupported model or missing required query) therefore triggers a second authenticated POST with a different body, adding request amplification and potentially obscuring the original error. Restrict the predicate to evidence of documents/return_documents versus texts/return_text incompatibility, or only retry when the response explicitly identifies an unknown/missing dialect field.

Suggestion:

Suggested change
if (await isLocalRerankSchemaValidationResponse(res)) {
res = await fetch(activeUrl, makeUpstreamInit("tei"));
}
if (await isLocalRerankSchemaValidationResponse(res)) {
res = await fetch(activeUrl, makeUpstreamInit("tei"));
}

Comment on lines +214 to +215
const systemInstruction = geminiBody.systemInstruction ?? geminiBody.system_instruction;
if (systemInstruction) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
Both field names are accepted, but conversion silently drops system_instruction whenever systemInstruction exists, while validation aggregates both. A request can therefore validate content that is never processed, and conflicting instructions have nondeterministic client-visible semantics. Reject the duplicate pair (including both casing variants on one part) or merge them explicitly before conversion.

Suggestion:

Suggested change
const systemInstruction = geminiBody.systemInstruction ?? geminiBody.system_instruction;
if (systemInstruction) {
if (geminiBody.systemInstruction && geminiBody.system_instruction) {
throw new Error("systemInstruction and system_instruction cannot both be provided");
}
const systemInstruction = geminiBody.systemInstruction ?? geminiBody.system_instruction;
if (systemInstruction) {

Comment on lines +92 to +94
if (activeClaim || !peekBootstrapToken(candidate)) return null;
activeClaim = Object.freeze({ kind: "bootstrap-token-claim" });
return activeClaim;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

performance · medium
A stalled or abandoned updateSettings() leaves this process-wide claim active, rejecting all later bootstrap writes; cancellation may not run this handler’s catch/release path. Ensure claim cleanup is bounded or persistence has a guaranteed timeout/finally release.

Suggestion:

Suggested change
if (activeClaim || !peekBootstrapToken(candidate)) return null;
activeClaim = Object.freeze({ kind: "bootstrap-token-claim" });
return activeClaim;
if (activeClaim || !peekBootstrapToken(candidate)) return null;
activeClaim = Object.freeze({ kind: "bootstrap-token-claim" });
return activeClaim;

Comment on lines +27 to +28
for (const key of ["inlineData", "inline_data"]) {
const inline = record[key];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bug · high
The aggregate/per-item byte checks count both inlineData and inline_data, but conversion uses only part.inlineData ?? part.inline_data; when both are supplied, the second payload is charged but ignored. Reject duplicate aliases or apply the same deterministic merge/rejection rule in both validation and conversion.

Suggestion:

Suggested change
for (const key of ["inlineData", "inline_data"]) {
const inline = record[key];
const camel = record.inlineData;
const snake = record.inline_data;
if (camel !== undefined && snake !== undefined) return [];
const inline = camel ?? snake;
if (inline && typeof inline === "object" && !Array.isArray(inline)) {
const data = (inline as { data?: unknown }).data;
return typeof data === "string" ? [data] : [];
}
return [];

Copy link
Copy Markdown
Owner Author

GoalKeeper evidence-bound CI triage (2026-10-21 ~06:38 ICT), NO SOURCE MUTATION:

  • Current PR fix: close AP-ISS-0197 API-route OCR findings #142 head c35014d5990b93739d99c675f436f589f564342c; GitHub CI run 39422618018 completed FAILURE. Other independent workflows (API Route Typecheck, semgrep, OpenCodeReview, DAST smoke, CodeQL) completed SUCCESS on this SHA.
  • Unit shard 1/8 job 118627800897: tests/unit/rerank-providers-route.test.ts:50, 'anonymous remote requests cannot list providers despite a localhost URL', actual HTTP 200 vs expected 401. This is one failed test among 5,538 reported by this shard.
  • Unit shard 5/8 job 118627801372: tests/unit/cache-config-test-embedding-ap0197.test.ts:49, 'anonymous request without a verified loopback peer remains unauthorized', actual HTTP 200 vs expected 401. This is one failed test among 4,854 reported by that shard.
  • Both failures relate to authorization/loopback-peer classification. Whether this is fixture leakage, request-context race, or production fail-open is UNKNOWN; reproduce each in isolated CI-equivalent conditions and test both loopback and untrusted remote peers before changing production auth. Do not weaken asserted 401 semantics merely to obtain green CI.
  • GoalKeeper host readback shows KitDev25 G16, AP-ISS-0197 remains CLAIMED with handoff BLOCKED and expired lease on 2026-10-21; no legal continuation should write under expired ownership. Preserve branch, PR, tests, source, and current WIP. Resume via validated single-worker recovery + exact conversation delivery/first output, not fabricated ACTIVE.

GoalKeeper 60m board logger newly started at 2026-10-21T06:37:18+07:00 with verified host writes to /home/coder/MCP/KitControl/runtime/scratch/gk-fullboard-20261021-0636.jsonl, expected stop 07:37:18 ICT. This PR comment is incident/evidence routing; not completion or review approval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant