From 9ce7b0551b5d4badfbf1855ae696dde79995f9dd Mon Sep 17 00:00:00 2001 From: Maxime Date: Fri, 9 Oct 2026 11:25:59 +0000 Subject: [PATCH 1/3] feat: read the custom instructions limit from PROVAL_USER_PROMPT_MAX_LENGTH The limit was a build-time constant, so every deployment that needed a longer custom prompt had to patch the source. It now resolves at runtime from PROVAL_USER_PROMPT_MAX_LENGTH: the API enforces the resolved value, announces it on /api/auth/me, and the repository form uses the announced limit instead of the bundled one. A value that is not a positive integer is ignored and the default applies. --- apps/api/src/api/auth/auth.service.ts | 2 ++ .../api/repository/repository.service.test.ts | 35 +++++++++++++++---- .../src/api/repository/repository.service.ts | 7 ++-- .../components/organism/RepositoryForm.svelte | 7 ++-- apps/client/src/lib/store/instance.ts | 14 ++++++++ apps/client/src/routes/+layout.svelte | 7 +++- apps/client/src/routes/+layout.ts | 2 ++ packages/types/src/database.ts | 1 + packages/types/src/index.ts | 2 +- packages/types/src/user-prompt.ts | 13 +++++++ 10 files changed, 76 insertions(+), 14 deletions(-) create mode 100644 apps/client/src/lib/store/instance.ts diff --git a/apps/api/src/api/auth/auth.service.ts b/apps/api/src/api/auth/auth.service.ts index c4c09237..1e41b8b5 100644 --- a/apps/api/src/api/auth/auth.service.ts +++ b/apps/api/src/api/auth/auth.service.ts @@ -7,6 +7,7 @@ import type { InstanceSettingUpdateInput, UserResponse, } from "@proval/types"; +import { resolveUserPromptMaxLength } from "@proval/types"; import db from "../../db/index.js"; const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000; @@ -72,6 +73,7 @@ export class AuthService { isAuthEnabled: setting.authEnabled, isRegistrationEnabled: setting.registrationEnabled, isSetupRequired, + userPromptMaxLength: resolveUserPromptMaxLength(process.env.PROVAL_USER_PROMPT_MAX_LENGTH), }; } diff --git a/apps/api/src/api/repository/repository.service.test.ts b/apps/api/src/api/repository/repository.service.test.ts index 3e50975e..1fa31fcc 100644 --- a/apps/api/src/api/repository/repository.service.test.ts +++ b/apps/api/src/api/repository/repository.service.test.ts @@ -1,5 +1,6 @@ import { beforeEach, describe, expect, it, mock } from "bun:test"; import type { Repository } from "@proval/types"; +import { USER_PROMPT_MAX_LENGTH } from "@proval/types"; process.env.ENCRYPTION_KEY = Buffer.alloc(32, 7).toString("base64"); @@ -306,9 +307,33 @@ describe("Update GitLab Repository", () => { }); it("rejects userPrompt longer than the max length", async () => { - await expect(repositoryService.update(1, { userPrompt: "a".repeat(2001) })).rejects.toThrow( - "Custom instructions must be at most 2000 characters", - ); + await expect( + repositoryService.update(1, { userPrompt: "a".repeat(USER_PROMPT_MAX_LENGTH + 1) }), + ).rejects.toThrow(`Custom instructions must be at most ${USER_PROMPT_MAX_LENGTH} characters`); + }); + + it("uses PROVAL_USER_PROMPT_MAX_LENGTH when the deployment sets it", async () => { + process.env.PROVAL_USER_PROMPT_MAX_LENGTH = "50"; + try { + await expect(repositoryService.update(1, { userPrompt: "a".repeat(51) })).rejects.toThrow( + "Custom instructions must be at most 50 characters", + ); + } finally { + delete process.env.PROVAL_USER_PROMPT_MAX_LENGTH; + } + }); + + it("ignores a PROVAL_USER_PROMPT_MAX_LENGTH that is not a positive integer", async () => { + for (const invalid of ["abc", "0", "-5", "1.5", ""]) { + process.env.PROVAL_USER_PROMPT_MAX_LENGTH = invalid; + try { + await expect( + repositoryService.update(1, { userPrompt: "a".repeat(USER_PROMPT_MAX_LENGTH + 1) }), + ).rejects.toThrow(`Custom instructions must be at most ${USER_PROMPT_MAX_LENGTH} characters`); + } finally { + delete process.env.PROVAL_USER_PROMPT_MAX_LENGTH; + } + } }); it("rejects userPrompt when it is not a string", async () => { @@ -318,9 +343,7 @@ describe("Update GitLab Repository", () => { }); it("rejects invalid reasoning effort", async () => { - await expect( - repositoryService.update(1, { reasoningEffort: "bogus" as "medium" }), - ).rejects.toThrow( + await expect(repositoryService.update(1, { reasoningEffort: "bogus" as "medium" })).rejects.toThrow( "Invalid reasoning effort", ); }); diff --git a/apps/api/src/api/repository/repository.service.ts b/apps/api/src/api/repository/repository.service.ts index 49accffc..dfb775b0 100644 --- a/apps/api/src/api/repository/repository.service.ts +++ b/apps/api/src/api/repository/repository.service.ts @@ -15,7 +15,7 @@ import { GitLabProvider } from "../../git-provider/gitlab.js"; import type { GitProvider } from "../../git-provider/types.js"; import { logError } from "../../util/log.js"; import { decrypt, encrypt } from "../../util/encrypt.js"; -import { reasoningEffortValueList, USER_PROMPT_MAX_LENGTH } from "@proval/types"; +import { reasoningEffortValueList, resolveUserPromptMaxLength } from "@proval/types"; import type { ReasoningEffort, Repository, @@ -458,8 +458,9 @@ export class RepositoryService { if (!trimmed) { return null; } - if (trimmed.length > USER_PROMPT_MAX_LENGTH) { - throw new Error(`Custom instructions must be at most ${USER_PROMPT_MAX_LENGTH} characters`); + const maxLength = resolveUserPromptMaxLength(process.env.PROVAL_USER_PROMPT_MAX_LENGTH); + if (trimmed.length > maxLength) { + throw new Error(`Custom instructions must be at most ${maxLength} characters`); } return trimmed; } diff --git a/apps/client/src/lib/components/organism/RepositoryForm.svelte b/apps/client/src/lib/components/organism/RepositoryForm.svelte index 5880a19c..63fc5018 100644 --- a/apps/client/src/lib/components/organism/RepositoryForm.svelte +++ b/apps/client/src/lib/components/organism/RepositoryForm.svelte @@ -1,7 +1,8 @@