Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import { VcsStatusBroadcaster } from "../src/vcs/VcsStatusBroadcaster.ts";
import { GitWorkflowService } from "../src/git/GitWorkflowService.ts";
import * as VcsProcess from "../src/vcs/VcsProcess.ts";
import * as AgentAwarenessRelay from "../src/relay/AgentAwarenessRelay.ts";
import * as IdentityService from "../src/identity/IdentityService.ts";

const decodeCodexSettings = Schema.decodeEffect(CodexSettings);

Expand Down Expand Up @@ -381,6 +382,7 @@ export const makeOrchestrationIntegrationHarness = (
Layer.provide(persistenceLayer),
Layer.provideMerge(RepositoryIdentityResolver.layer),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(IdentityService.layerWithPeople([])),
Layer.provideMerge(ServerConfig.layerTest(workspaceDir, rootDir)),
Layer.provideMerge(NodeServices.layer),
);
Expand Down
36 changes: 36 additions & 0 deletions apps/server/src/identity/agentAttribution.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { IdentityUsername, PersonId, type SourceRef } from "@t3tools/contracts";
import { describe, expect, it } from "vite-plus/test";

import { withAgentIdentityAttribution } from "./agentAttribution.ts";

const people = [
{
personId: "patroza",
username: "patroza",
name: "Patrick Roza",
github: { login: "patroza", id: "42661" },
},
] as const;

describe("withAgentIdentityAttribution", () => {
it.each(["desktop", "web", "discord", "jira", "github"] as const)(
"applies the mapped identity to %s turns",
(channel) => {
const source: SourceRef = {
channel,
personId: PersonId.make("patroza"),
username: IdentityUsername.make("patroza"),
};
const result = withAgentIdentityAttribution({ message: "make the change", source, people });

expect(result).toContain("The server identity map attributes this turn to patroza");
expect(result).toContain(
"Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>",
);
},
);

it("leaves unattributed turns unchanged", () => {
expect(withAgentIdentityAttribution({ message: "hello", people })).toBe("hello");
});
});
22 changes: 22 additions & 0 deletions apps/server/src/identity/agentAttribution.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import type { SourceRef } from "@t3tools/contracts";
import {
findPersonByPersonId,
formatCoAuthoredByTrailer,
type IdentityMapPerson,
} from "@t3tools/shared/identityMap";

/** Add trusted commit attribution to an agent turn, regardless of its channel. */
export function withAgentIdentityAttribution(input: {
readonly message: string;
readonly source?: SourceRef | undefined;
readonly people: ReadonlyArray<IdentityMapPerson>;
}): string {
const personId = input.source?.personId;
if (personId === undefined) return input.message;
const person = findPersonByPersonId(input.people, personId);
if (person === null) return input.message;
const trailer = formatCoAuthoredByTrailer(person);
if (trailer === null) return input.message;

return `${input.message}\n\n<identity_attribution>\nThe server identity map attributes this turn to ${person.username}. Every git commit created for this work must include this exact trailer after a blank line:\n${trailer}\nKeep the environment's default author and committer.\n</identity_attribution>`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ import {
CommandId,
DEFAULT_PROVIDER_INTERACTION_MODE,
EventId,
IdentityUsername,
MessageId,
PersonId,
ProjectId,
ThreadId,
TurnId,
Expand Down Expand Up @@ -68,6 +70,8 @@ import * as Clock from "effect/Clock";
import { ServerSettingsService } from "../../serverSettings.ts";
import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";
import * as GitWorkflowService from "../../git/GitWorkflowService.ts";
import * as IdentityService from "../../identity/IdentityService.ts";
import type { IdentityMapPerson } from "@t3tools/shared/identityMap";

const asProjectId = (value: string): ProjectId => ProjectId.make(value);
const asApprovalRequestId = (value: string): ApprovalRequestId => ApprovalRequestId.make(value);
Expand Down Expand Up @@ -168,6 +172,7 @@ describe("ProviderCommandReactor", () => {
session: ProviderSession,
) => Effect.Effect<ProviderSession, ProviderAdapterRequestError>;
readonly interruptTurnEffect?: ProviderServiceShape["interruptTurn"];
readonly identityPeople?: ReadonlyArray<IdentityMapPerson>;
}) {
const now = "2026-01-01T00:00:00.000Z";
const baseDir =
Expand Down Expand Up @@ -438,6 +443,7 @@ describe("ProviderCommandReactor", () => {
}),
),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(IdentityService.layerWithPeople(input?.identityPeople ?? [])),
Layer.provideMerge(ServerConfig.layerTest(process.cwd(), baseDir)),
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(persistenceLayer),
Expand Down Expand Up @@ -649,6 +655,45 @@ describe("ProviderCommandReactor", () => {
expect(harness.sendTurn).toHaveBeenCalledTimes(1);
});

it("sends desktop turns with identity-map commit attribution", async () => {
const harness = await createHarness({
identityPeople: [
{
personId: "patroza",
username: "patroza",
name: "Patrick Roza",
github: { login: "patroza", id: "42661" },
},
],
});
await harness.dispatch({
type: "thread.turn.start",
commandId: CommandId.make("cmd-attributed-turn"),
threadId: ThreadId.make("thread-1"),
message: {
messageId: asMessageId("attributed-user-message"),
role: "user",
text: "ship this",
attachments: [],
},
source: {
channel: "desktop",
personId: PersonId.make("patroza"),
username: IdentityUsername.make("patroza"),
},
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
runtimeMode: "approval-required",
createdAt: "2026-01-01T00:00:00.000Z",
});

await waitFor(() => harness.sendTurn.mock.calls.length === 1);
expect(harness.sendTurn.mock.calls[0]?.[0]).toMatchObject({
input: expect.stringContaining(
"Co-authored-by: Patrick Roza <42661+patroza@users.noreply.github.com>",
),
});
});

it("continues an interrupted running turn without replaying its user message", async () => {
const modelSelection: ModelSelection = {
instanceId: ProviderInstanceId.make("codex"),
Expand Down
11 changes: 10 additions & 1 deletion apps/server/src/orchestration/Layers/ProviderCommandReactor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ import {
} from "../../serverSettings.ts";
import { VcsStatusBroadcaster } from "../../vcs/VcsStatusBroadcaster.ts";
import { GitWorkflowService } from "../../git/GitWorkflowService.ts";
import { IdentityService } from "../../identity/IdentityService.ts";
import { withAgentIdentityAttribution } from "../../identity/agentAttribution.ts";

const PROVIDER_CONTROL_TIMEOUT = Duration.seconds(5);
const isProviderAdapterRequestError = Schema.is(ProviderAdapterRequestError);
Expand Down Expand Up @@ -285,6 +287,7 @@ const make = Effect.gen(function* () {
const vcsStatusBroadcaster = yield* VcsStatusBroadcaster;
const textGeneration = yield* TextGeneration;
const serverSettingsService = yield* ServerSettingsService;
const identityService = yield* IdentityService;
const serverCommandId = (tag: string) =>
crypto.randomUUIDv4.pipe(Effect.map((uuid) => CommandId.make(`server:${tag}:${uuid}`)));
const serverEventId = () => crypto.randomUUIDv4.pipe(Effect.map(EventId.make));
Expand Down Expand Up @@ -1148,9 +1151,15 @@ const make = Effect.gen(function* () {
),
);

const identityPeople = yield* identityService.listMapPeople();
const attributedMessageText = withAgentIdentityAttribution({
message: message.text,
source: message.source,
people: identityPeople,
});
const sendTurnRequest = yield* buildSendTurnRequestForThread({
threadId: event.payload.threadId,
messageText: message.text,
messageText: attributedMessageText,
...(message.attachments !== undefined ? { attachments: message.attachments } : {}),
...(event.payload.modelSelection !== undefined
? { modelSelection: event.payload.modelSelection }
Expand Down
29 changes: 29 additions & 0 deletions packages/shared/src/identityMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,35 @@ export function findPersonByDiscordId(
return people.find((person) => person.discord?.id === id) ?? null;
}

/** Resolve the canonical person stamped onto interactive and integration turns. */
export function findPersonByPersonId(
people: ReadonlyArray<IdentityMapPerson>,
personId: string,
): IdentityMapPerson | null {
const normalized = personId.trim().toLowerCase();
if (normalized.length === 0) return null;
return people.find((person) => person.personId.toLowerCase() === normalized) ?? null;
}

/** Full Git trailer for a mapped person, when GitHub attribution is configured. */
export function formatCoAuthoredByTrailer(person: IdentityMapPerson): string | null {
const github = person.github;
if (github === undefined) return null;
const explicitEmail = github.email?.trim();
const id = github.id?.trim();
const login = github.login.trim();
const email =
explicitEmail && explicitEmail.length > 0
? explicitEmail
: id && id.length > 0 && login.length > 0
? `${id}+${login}@users.noreply.github.com`
: null;
if (email === null) return null;
const rawName = github.name ?? person.name ?? person.username;
const name = rawName.replace(/[\r\n]+/gu, " ").trim();
return name.length > 0 ? `Co-authored-by: ${name} <${email}>` : null;
}

export function findPersonByGithubLogin(
people: ReadonlyArray<IdentityMapPerson>,
login: string,
Expand Down
Loading