From cf4f77f275dfc42ae54c184afcf602d042fc2e42 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Sat, 12 Sep 2026 14:06:57 -0400 Subject: [PATCH] fix(rpc): reject a blank GitHub owner or repo normalizeTaskProviderIdentity treats a blank owner or repo as no identity at all, but the schema accepted '' and whitespace-only, so the two disagreed about the same payload. Refined rather than trimmed: trimming would rewrite the parsed value and change what the handler receives. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- .../methods/task-provider-identity.test.ts | 31 +++++++++++++++++++ src/shared/rpc-contract/automation-params.ts | 12 +++++-- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/main/runtime/rpc/methods/task-provider-identity.test.ts b/src/main/runtime/rpc/methods/task-provider-identity.test.ts index b6ec00ae6ab..1cf6d54381c 100644 --- a/src/main/runtime/rpc/methods/task-provider-identity.test.ts +++ b/src/main/runtime/rpc/methods/task-provider-identity.test.ts @@ -108,3 +108,34 @@ describe('task provider identity RPC validation', () => { ).toBe(false) }) }) + +describe('github identity blank fields', () => { + // The normalizer treats a blank owner or repo as no identity, so the schema must agree. + it.each(['', ' ', '\t'])('rejects a blank owner %j', (owner) => { + expect( + TaskProviderIdentity.safeParse({ provider: 'github', owner, repo: 'orca' }).success + ).toBe(false) + }) + + it.each(['', ' '])('rejects a blank repo %j', (repo) => { + expect( + TaskProviderIdentity.safeParse({ provider: 'github', owner: 'stablyai', repo }).success + ).toBe(false) + }) + + it('still accepts a populated identity', () => { + expect( + TaskProviderIdentity.safeParse({ provider: 'github', owner: 'stablyai', repo: 'orca' }) + .success + ).toBe(true) + }) + + it('leaves the parsed value untrimmed, so no wire bytes change', () => { + const parsed = TaskProviderIdentity.safeParse({ + provider: 'github', + owner: ' stablyai ', + repo: 'orca' + }) + expect(parsed.success && parsed.data?.owner).toBe(' stablyai ') + }) +}) diff --git a/src/shared/rpc-contract/automation-params.ts b/src/shared/rpc-contract/automation-params.ts index 4fb11186ce4..d2ddd4aaad6 100644 --- a/src/shared/rpc-contract/automation-params.ts +++ b/src/shared/rpc-contract/automation-params.ts @@ -57,13 +57,21 @@ export const OptionalNullablePlainString = z .pipe(z.union([z.string(), z.null(), z.undefined()])) .optional() +// A GitHub identity is only usable with both fields present and non-blank. +const GithubIdentityField = z.string().refine((value) => value.trim().length > 0, { + message: 'Required' +}) + export const TaskProviderIdentity = z .discriminatedUnion('provider', [ z .object({ provider: z.literal('github'), - owner: z.string(), - repo: z.string(), + // Why refine, not .trim(): normalizeTaskProviderIdentity treats a blank owner or repo as + // no identity at all, so blank must be rejected here — but trimming would rewrite the + // parsed value and change what the handler receives. + owner: GithubIdentityField, + repo: GithubIdentityField, host: z.string().optional() }) .passthrough(),