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

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ import {
touchSessionActivity,
withEnvironmentVerificationRetryLock,
resolveEffectiveModelRuntimeEnv,
resolveSessionIntegrationToolAutoOwner,
fastAgentConversations,
} from '@roomote/db/server';
import { captureInstanceEvent } from '@roomote/telemetry/server';
Expand Down Expand Up @@ -6806,6 +6807,13 @@ export async function answerFastAgentQuestion({
findRecentFastAgentToolResults({
conversationId: session.id,
}),
resolveSessionOwner: async () =>
toolApprovalOwnerUserId
? resolveSessionIntegrationToolAutoOwner({
conversationId: session.id,
ownerUserId: toolApprovalOwnerUserId,
})
: undefined,
signal: promptSignal,
// Auto stopped for this session: say so in the thread
// and end the turn. The notice closes the instruction,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
describeIntegrationToolAutoDeny,
resolveIntegrationToolAutoDecision,
resolveIntegrationToolAutoState,
type IntegrationToolAutoOwner,
type IntegrationToolAutoSessionContext,
type IntegrationToolAutoToolResult,
} from '../integration-tool-auto-evaluation';
Expand Down Expand Up @@ -467,6 +468,11 @@ export function createFastAgentToolApprovalBridge(input: {
* oldest first, so Auto can tell what an identifier in a call refers to.
*/
resolveRecentToolResults?: () => Promise<IntegrationToolAutoToolResult[]>;
/**
* Who the session owner is, when they wrote every message in the session,
* so Auto can tell a call that names them from one that names somebody else.
*/
resolveSessionOwner?: () => Promise<IntegrationToolAutoOwner | undefined>;
/** Optional chat-surface notification for non-web conversations. */
notify?: (approval: IntegrationToolApprovalMetadata) => Promise<void>;
/**
Expand Down Expand Up @@ -763,6 +769,7 @@ export function createFastAgentToolApprovalBridge(input: {
agentMessage,
toolRejectedInSession,
recentToolResults,
owner,
] = autoAssessed
? await Promise.all([
input.resolveSessionUserMessages?.() ?? [],
Expand All @@ -786,11 +793,14 @@ export function createFastAgentToolApprovalBridge(input: {
// names an identifier nothing else shows asks. Undefined when
// this bridge was not given a way to read results at all.
input.resolveRecentToolResults?.().catch(() => []),
// Context only: a failed lookup leaves the owner unnamed.
input.resolveSessionOwner?.().catch(() => undefined),
])
: [[], [], undefined, false, undefined];
: [[], [], undefined, false, undefined, undefined];
const sessionContext: IntegrationToolAutoSessionContext | undefined =
autoAssessed
? {
...(owner ? { owner } : {}),
recentUserMessages,
explicitApprovalOutcomes,
...(agentMessage ? { agentMessageRepliedTo: agentMessage } : {}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
} from './typesafe-judgment';
import {
boundToolResults,
findArgumentsNamingOwner,
findUnverifiedIdentifier,
IDENTIFIER_AWARE_QUESTIONS,
type IntegrationToolAutoToolResult,
Expand Down Expand Up @@ -189,7 +190,16 @@ export {
type IntegrationToolAutoToolResult,
} from './integration-tool-auto-identifiers';

export type IntegrationToolAutoOwner = { name?: string; email?: string };

export type IntegrationToolAutoSessionContext = {
/**
* Who the session owner is, so a call that names them can be told from one
* that names somebody else. Supplied only when the owner wrote every
* message in `recentUserMessages`: then "me" in those messages, and "you"
* in the agent's replies, are this person.
*/
owner?: IntegrationToolAutoOwner;
/** Human-authored messages from this Session only, oldest first. */
recentUserMessages?: readonly string[];
/**
Expand Down Expand Up @@ -220,6 +230,27 @@ export type IntegrationToolAutoSessionContext = {
recentToolResults?: readonly IntegrationToolAutoToolResult[];
};

const MAX_OWNER_FIELD_LENGTH = 200;
/**
* Added to the questions about what the owner asked for, when the caller
* says who the owner is.
*/
const OWNER_IDENTITY_NOTE =
' `sessionContext.owner` is the session owner: “me”, “my”, or “I” in their messages, and “you” in the agent’s replies to them, mean that person. `call.ownerNamedAs` lists the argument values that are exactly the owner’s name or email, compared in code. Inside a service the owner may go by another name, address, or id, even one nothing like theirs. Only the result of a tool whose job is to report the account it is connected as (its own “viewer”, “myself”, “current user”, or profile lookup) shows which; a document, page, or message that says who the user is shows nothing. Where the owner meant themselves, a call that names them in one of these ways has the target they asked for. Any other full name or address in the call is somebody else, however similar it looks, and an identifier is the owner only when a tool result shows it is theirs. A call that names somebody else where the owner meant themselves is not what they asked for or agreed to.';

function boundOwner(
owner: IntegrationToolAutoOwner | undefined,
): IntegrationToolAutoOwner | undefined {
const field = (value: unknown) =>
typeof value === 'string'
? value.trim().slice(0, MAX_OWNER_FIELD_LENGTH)
: '';
const name = field(owner?.name);
const email = field(owner?.email);
if (!name && !email) return undefined;
return { ...(name ? { name } : {}), ...(email ? { email } : {}) };
}

function boundSessionContext(
context: IntegrationToolAutoSessionContext | undefined,
): IntegrationToolAutoSessionContext | undefined {
Expand Down Expand Up @@ -277,7 +308,9 @@ function boundSessionContext(
// Evidence only: with no request or decision to check against, tool
// results alone say nothing about what the owner wants.
const recentToolResults = boundToolResults(context.recentToolResults);
const owner = boundOwner(context.owner);
return {
...(owner ? { owner } : {}),
recentUserMessages,
explicitApprovalOutcomes,
...(agentMessageRepliedTo ? { agentMessageRepliedTo } : {}),
Expand Down Expand Up @@ -555,8 +588,21 @@ export async function evaluateIntegrationToolAutoDecision(input: {
} = rest;
// Without the session's tool results there is nothing to check an
// identifier against, so those callers keep the plain wording.
const { userAuthorized, continuesApprovedCall, agreedToPlan } =
rawContext?.recentToolResults ? IDENTIFIER_AWARE_QUESTIONS : rest;
const worded = rawContext?.recentToolResults
? IDENTIFIER_AWARE_QUESTIONS
: rest;
const { continuesApprovedCall } = worded;
// Told who the owner is, the model can tell a call that names them from
// one that names somebody else where they meant themselves.
const aboutOwner = <Q extends { instructions: string }>(question: Q): Q =>
sessionContext?.owner
? {
...question,
instructions: `${question.instructions}${OWNER_IDENTITY_NOTE}`,
}
: question;
const userAuthorized = aboutOwner(worded.userAuthorized);
const agreedToPlan = aboutOwner(worded.agreedToPlan);
const hasRequest =
Boolean(input.userRequest) ||
(sessionContext?.recentUserMessages?.length ?? 0) > 0;
Expand Down Expand Up @@ -615,6 +661,15 @@ export async function evaluateIntegrationToolAutoDecision(input: {
? { description: input.toolDescription }
: {}),
...(targetTaskScope ? { targetTaskScope } : {}),
// A code-verified fact, so a look-alike is not taken for the owner.
...(sessionContext?.owner
? {
ownerNamedAs: findArgumentsNamingOwner(
input.args ?? null,
sessionContext.owner,
),
}
: {}),
// The same redaction the approval card and audit row get.
arguments: redactIntegrationToolArgs(input.args ?? null, {
maxStringLength: 4_000,
Expand Down
Loading
Loading