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
3 changes: 3 additions & 0 deletions apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
} from "@t3tools/contracts";
import type * as RpcGroup from "effect/unstable/rpc/RpcGroup";

import { AGENT_PERSONA_RPC_SCOPES } from "../j5/agents/agentPersonaRpc.ts";

type WsRpcMethod = RpcGroup.Rpcs<typeof WsRpcGroup>["_tag"];

/**
Expand All @@ -21,6 +23,7 @@ type WsRpcMethod = RpcGroup.Rpcs<typeof WsRpcGroup>["_tag"];
* runtime failure.
*/
export const RPC_REQUIRED_SCOPES = {
...AGENT_PERSONA_RPC_SCOPES,
[ORCHESTRATION_V2_WS_METHODS.dispatchCommand]: AuthOrchestrationOperateScope,
[ORCHESTRATION_V2_WS_METHODS.getWorkflowScript]: AuthOrchestrationReadScope,
[ORCHESTRATION_V2_WS_METHODS.getTurnDiff]: AuthOrchestrationReadScope,
Expand Down
60 changes: 60 additions & 0 deletions apps/server/src/j5/agents/agentPersonaAdapterInstructions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { assert, describe, it } from "@effect/vitest";
import { type ModelSelection, ProviderInstanceId } from "@t3tools/contracts";
import * as Effect from "effect/Effect";

import {
CLAUDE_PROVIDER,
makeClaudeQueryOptions,
} from "../../orchestration-v2/Adapters/ClaudeAdapterV2.ts";
import { buildCodexTurnStartParams } from "../../orchestration-v2/Adapters/CodexAdapterV2.ts";
import { T3_CODE_ORCHESTRATION_INSTRUCTIONS } from "../../provider/T3OrchestrationInstructions.ts";

const CLAUDE_TEST_MODEL_SELECTION = {
instanceId: ProviderInstanceId.make(CLAUDE_PROVIDER),
model: "claude-sonnet-4-6",
options: [{ id: "effort", value: "ultrathink" }],
} satisfies ModelSelection;

describe("agent persona adapter instructions", () => {
it("adds persona instructions to Claude's system prompt without requiring MCP", () => {
const options = makeClaudeQueryOptions({
modelSelection: CLAUDE_TEST_MODEL_SELECTION,
nativeThreadId: "native-builder-instructions",
resume: false,
cwd: "/workspace",
agentPersonaInstructions: "Builder instructions",
});

assert.isObject(options.systemPrompt);
const systemPrompt = options.systemPrompt as { readonly append?: string };
assert.include(systemPrompt.append ?? "", "Builder instructions");
assert.include(systemPrompt.append ?? "", "<runtime_info>");
assert.notInclude(systemPrompt.append ?? "", T3_CODE_ORCHESTRATION_INSTRUCTIONS);
});

it.effect("adds persona instructions without requiring the T3 MCP server", () =>
Effect.gen(function* () {
const params = yield* buildCodexTurnStartParams({
nativeThreadId: "native-builder-instructions",
codexInput: [{ type: "text", text: "implement this handoff" }],
runtimePolicy: {
runtimeMode: "auto-accept-edits",
interactionMode: "default",
cwd: "/workspace",
agentPersonaInstructions: "Builder instructions",
},
modelSelection: {
instanceId: ProviderInstanceId.make("codex"),
model: "gpt-5.6-sol",
},
hasT3Mcp: false,
});

assert.equal(params.collaborationMode?.mode, "default");
assert.equal(
params.collaborationMode?.settings.developer_instructions,
"Builder instructions",
);
}),
);
});
16 changes: 8 additions & 8 deletions apps/server/src/j5/agents/agentPersonaAssignment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { assert, describe, it } from "@effect/vitest";
import { ProviderDriverKind, ProviderInstanceId } from "@t3tools/contracts";

import {
buildBuiltInAgentPersonaAssignment,
validateBuiltInAgentPersonaAssignment,
buildAgentPersonaAssignment,
validateAgentPersonaAssignment,
} from "./agentPersonaAssignment.ts";

const criticRoute = {
Expand All @@ -22,7 +22,7 @@ const criticRoute = {

describe("agent persona assignment", () => {
it("snapshots the default authority and resolved route", () => {
const result = buildBuiltInAgentPersonaAssignment({ resolution: criticRoute });
const result = buildAgentPersonaAssignment({ resolution: criticRoute });
assert.equal(result.status, "assigned");
if (result.status !== "assigned") return;
assert.deepEqual(result.assignment, {
Expand All @@ -36,7 +36,7 @@ describe("agent persona assignment", () => {
});

it("blocks Critic Fix Mode on a provider that cannot enforce workspace authority", () => {
const result = buildBuiltInAgentPersonaAssignment({
const result = buildAgentPersonaAssignment({
resolution: criticRoute,
authorityPolicy: "critic-fix",
});
Expand All @@ -49,7 +49,7 @@ describe("agent persona assignment", () => {
});

it("rejects an authority policy outside the persona contract", () => {
const result = buildBuiltInAgentPersonaAssignment({
const result = buildAgentPersonaAssignment({
resolution: criticRoute,
authorityPolicy: "workspace-write",
});
Expand All @@ -63,7 +63,7 @@ describe("agent persona assignment", () => {

it("rejects forged assignments that combine a persona with elevated authority", () => {
assert.equal(
validateBuiltInAgentPersonaAssignment({
validateAgentPersonaAssignment({
personaId: "scout",
definitionVersion: 1,
authorityPolicy: "publish-only",
Expand All @@ -80,9 +80,9 @@ describe("agent persona assignment", () => {
});

it("accepts a server-built assignment that matches the declared route", () => {
const result = buildBuiltInAgentPersonaAssignment({ resolution: criticRoute });
const result = buildAgentPersonaAssignment({ resolution: criticRoute });
assert.equal(result.status, "assigned");
if (result.status !== "assigned") return;
assert.isUndefined(validateBuiltInAgentPersonaAssignment(result.assignment));
assert.isUndefined(validateAgentPersonaAssignment(result.assignment));
});
});
14 changes: 8 additions & 6 deletions apps/server/src/j5/agents/agentPersonaAssignment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@ import {
getBuiltInAgentPersona,
type AgentAuthorityPolicyId,
type AgentPersonaId,
type AgentPersonaDefinition,
} from "./agentPersonas.ts";

type AvailableAgentPersonaRoute = Extract<AgentPersonaRouteResolution, { status: "available" }>;

export type BuiltInAgentPersonaAssignmentResult =
export type AgentPersonaAssignmentResult =
| {
readonly status: "assigned";
readonly assignment: OrchestrationV2AgentPersonaAssignment;
Expand All @@ -28,11 +29,12 @@ export type BuiltInAgentPersonaAssignmentResult =
readonly driver: AvailableAgentPersonaRoute["driver"];
};

export function buildBuiltInAgentPersonaAssignment(input: {
export function buildAgentPersonaAssignment(input: {
readonly resolution: AvailableAgentPersonaRoute;
readonly definition?: AgentPersonaDefinition;
readonly authorityPolicy?: AgentAuthorityPolicyId;
}): BuiltInAgentPersonaAssignmentResult {
const definition = getBuiltInAgentPersona(input.resolution.personaId);
}): AgentPersonaAssignmentResult {
const definition = input.definition ?? getBuiltInAgentPersona(input.resolution.personaId);
const authorityPolicy = input.authorityPolicy ?? definition.authority.defaultPolicy;
if (!definition.authority.allowedPolicies.some((policy) => policy === authorityPolicy)) {
return {
Expand Down Expand Up @@ -64,10 +66,10 @@ export function buildBuiltInAgentPersonaAssignment(input: {
};
}

export function validateBuiltInAgentPersonaAssignment(
export function validateAgentPersonaAssignment(
assignment: OrchestrationV2AgentPersonaAssignment,
definition: AgentPersonaDefinition = getBuiltInAgentPersona(assignment.personaId),
): string | undefined {
const definition = getBuiltInAgentPersona(assignment.personaId);
const target = definition.modelRoute[assignment.resolvedRoute === "primary" ? 0 : 1];
const optionId = target.driver === "codex" ? "reasoningEffort" : "effort";
const selectedEffort = assignment.resolvedModelSelection.options?.find(
Expand Down
Loading
Loading