From 3b4637e99f7b2d88a7829f606f9184d66009fea4 Mon Sep 17 00:00:00 2001 From: tusharbhardwaj-bk Date: Fri, 21 Aug 2026 07:28:24 +0000 Subject: [PATCH] fix(server): use T3 sender for Claude userEmail Claude's native prompt reports the shared account email, which misattributes the user on Beknown's shared runtime. Append the session-scoped message sender as the authoritative userEmail, and explicitly preserve unknown identity when no sender is resolved. TEC-1031 --- .../src/provider/Layers/ClaudeAdapter.ts | 13 ++++++- .../claudeSessionIdentity.expbkt3.test.ts | 39 +++++++++++++++++++ .../provider/claudeSessionIdentity.expbkt3.ts | 34 ++++++++++++++++ docs/operations/expbkt3-customizations.md | 8 ++++ 4 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 apps/server/src/provider/claudeSessionIdentity.expbkt3.test.ts create mode 100644 apps/server/src/provider/claudeSessionIdentity.expbkt3.ts diff --git a/apps/server/src/provider/Layers/ClaudeAdapter.ts b/apps/server/src/provider/Layers/ClaudeAdapter.ts index 9843b52f5399..0f26b54099ee 100644 --- a/apps/server/src/provider/Layers/ClaudeAdapter.ts +++ b/apps/server/src/provider/Layers/ClaudeAdapter.ts @@ -102,6 +102,8 @@ import { import { makeObservableLifecycle } from "../observableLifecycle.ts"; import { type ClaudeAdapterShape } from "../Services/ClaudeAdapter.ts"; import { type EventNdjsonLogger, makeEventNdjsonLogger } from "./EventNdjsonLogger.ts"; +// T3-CUSTOM(expbkt3): Claude's shared account email is not the T3 message sender. +import { claudeSessionIdentitySystemPrompt } from "../claudeSessionIdentity.expbkt3.ts"; const encodeUnknownJsonStringExit = Schema.encodeUnknownExit(Schema.fromJsonString(Schema.Unknown)); const decodeUnknownJsonStringExit = Schema.decodeUnknownExit(Schema.fromJsonString(Schema.Unknown)); @@ -4193,11 +4195,20 @@ export const makeClaudeAdapter = Effect.fn("makeClaudeAdapter")(function* ( ...(input.cwd ? [input.cwd] : []), serverConfig.attachmentsDir, ]; + // T3-CUSTOM(expbkt3): BEGIN override Claude's shared-account userEmail context. + const sessionIdentitySystemPrompt = claudeSessionIdentitySystemPrompt(sessionEnvironment); + // T3-CUSTOM(expbkt3): END const queryOptions: ClaudeQueryOptions = { ...(input.cwd ? { cwd: input.cwd } : {}), ...(apiModelId ? { model: apiModelId } : {}), pathToClaudeCodeExecutable: claudeBinaryPath, - systemPrompt: { type: "preset", preset: "claude_code" }, + // T3-CUSTOM(expbkt3): BEGIN preserve the native prompt with T3 sender identity appended. + systemPrompt: { + type: "preset", + preset: "claude_code", + ...(sessionIdentitySystemPrompt ? { append: sessionIdentitySystemPrompt } : {}), + }, + // T3-CUSTOM(expbkt3): END settingSources: [...CLAUDE_SETTING_SOURCES], // `ultracode` is a Claude Code setting, not an API effort level. It is // normalized to `xhigh` above and paired with `settings.ultracode`. diff --git a/apps/server/src/provider/claudeSessionIdentity.expbkt3.test.ts b/apps/server/src/provider/claudeSessionIdentity.expbkt3.test.ts new file mode 100644 index 000000000000..86027153c011 --- /dev/null +++ b/apps/server/src/provider/claudeSessionIdentity.expbkt3.test.ts @@ -0,0 +1,39 @@ +import { assert, describe, it } from "@effect/vitest"; + +import { claudeSessionIdentitySystemPrompt } from "./claudeSessionIdentity.expbkt3.ts"; + +describe("claudeSessionIdentitySystemPrompt", () => { + it("uses the current T3 message sender instead of the shared Claude account", () => { + assert.equal( + claudeSessionIdentitySystemPrompt({ + BK_IDENTITY_RUNTIME: "t3-code", + BK_SESSION_OWNER_EMAIL: "owner@example.test", + BK_MESSAGE_SENDER_EMAIL: " sender@example.test ", + }), + [ + "T3 Code session identity:", + '- userEmail is "sender@example.test".', + "- This session-scoped value identifies the user who sent the current message and overrides the Claude account email for user attribution.", + ].join("\n"), + ); + }); + + it("keeps the user unknown when T3 cannot resolve the message sender", () => { + assert.include( + claudeSessionIdentitySystemPrompt({ + BK_IDENTITY_RUNTIME: "t3-code", + BK_SESSION_OWNER_EMAIL: "owner@example.test", + }) ?? "", + "userEmail is unavailable", + ); + }); + + it("does not change upstream Claude sessions", () => { + assert.equal( + claudeSessionIdentitySystemPrompt({ + BK_MESSAGE_SENDER_EMAIL: "sender@example.test", + }), + undefined, + ); + }); +}); diff --git a/apps/server/src/provider/claudeSessionIdentity.expbkt3.ts b/apps/server/src/provider/claudeSessionIdentity.expbkt3.ts new file mode 100644 index 000000000000..0dccfd740a66 --- /dev/null +++ b/apps/server/src/provider/claudeSessionIdentity.expbkt3.ts @@ -0,0 +1,34 @@ +// T3-CUSTOM(expbkt3): Claude Code derives its native `userEmail` context from +// the authenticated Claude account. Beknown runs that account on a shared +// machine, so it identifies the subscription rather than the person sending +// the current T3 message. Append the session-scoped identity to Claude's native +// system prompt without changing the account used for authentication. + +import { + MESSAGE_SENDER_EMAIL_KEY, + SESSION_IDENTITY_RUNTIME, + SESSION_IDENTITY_RUNTIME_KEY, +} from "../identity/SessionIdentityEnvironment.ts"; + +export function claudeSessionIdentitySystemPrompt( + environment: NodeJS.ProcessEnv, +): string | undefined { + if (environment[SESSION_IDENTITY_RUNTIME_KEY] !== SESSION_IDENTITY_RUNTIME) { + return undefined; + } + + const senderEmail = environment[MESSAGE_SENDER_EMAIL_KEY]?.trim(); + if (!senderEmail) { + return [ + "T3 Code session identity:", + "- userEmail is unavailable for the user who sent the current message.", + "- Do not use the Claude account email, operating-system identity, or Git identity to infer the user.", + ].join("\n"); + } + + return [ + "T3 Code session identity:", + `- userEmail is ${JSON.stringify(senderEmail)}.`, + "- This session-scoped value identifies the user who sent the current message and overrides the Claude account email for user attribution.", + ].join("\n"); +} diff --git a/docs/operations/expbkt3-customizations.md b/docs/operations/expbkt3-customizations.md index c6f206c2a584..a1e2bc96242a 100644 --- a/docs/operations/expbkt3-customizations.md +++ b/docs/operations/expbkt3-customizations.md @@ -395,6 +395,14 @@ Four rules carry the behaviour: live session was started with and restarts on an owner transfer or a new sender, next to the existing credential-actor restart. +Claude Code's native system prompt normally derives `userEmail` from the +authenticated Claude account. That account is shared in the Beknown runtime, so +the Claude adapter appends the resolved `BK_MESSAGE_SENDER_EMAIL` as the +authoritative `userEmail`. When the sender is unresolved, the appended context +explicitly leaves `userEmail` unknown and forbids inference from the shared +Claude account, operating-system identity, or Git identity. Non-T3 Claude +sessions keep the upstream system prompt unchanged. + The markers compose with source-control profiles rather than replacing them: `mergeSourceControlEnvironment` scrubs the machine's inherited Git and GitHub credentials only when the overlay carries a source-control identity of its own,