fix(team): satisfy bootstrap redaction lint
This commit is contained in:
parent
2a41010610
commit
2080e86f44
12 changed files with 218 additions and 32 deletions
|
|
@ -90,8 +90,8 @@ import {
|
||||||
} from '@shared/constants';
|
} from '@shared/constants';
|
||||||
import { shouldSuppressDesktopNotificationForInboxText } from '@shared/utils/idleNotificationSemantics';
|
import { shouldSuppressDesktopNotificationForInboxText } from '@shared/utils/idleNotificationSemantics';
|
||||||
import { parseInboxJson } from '@shared/utils/inboxNoise';
|
import { parseInboxJson } from '@shared/utils/inboxNoise';
|
||||||
import { isTeamInternalControlMessageText } from '@shared/utils/teamInternalControlMessages';
|
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
|
import { isTeamInternalControlMessageEnvelope } from '@shared/utils/teamInternalControlMessages';
|
||||||
import { app, BrowserWindow, ipcMain } from 'electron';
|
import { app, BrowserWindow, ipcMain } from 'electron';
|
||||||
import { existsSync } from 'fs';
|
import { existsSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
@ -473,7 +473,7 @@ async function notifyNewInboxMessages(teamName: string, detail: string): Promise
|
||||||
if (msg.source && suppressedSources.has(msg.source)) continue;
|
if (msg.source && suppressedSources.has(msg.source)) continue;
|
||||||
// Skip app-owned private bootstrap/control prompts. They are durable runtime proof inputs,
|
// Skip app-owned private bootstrap/control prompts. They are durable runtime proof inputs,
|
||||||
// not user-visible conversation messages.
|
// not user-visible conversation messages.
|
||||||
if (isTeamInternalControlMessageText(msg.text)) continue;
|
if (isTeamInternalControlMessageEnvelope(msg)) continue;
|
||||||
// Skip internal coordination noise (idle_notification, shutdown_*, etc.)
|
// Skip internal coordination noise (idle_notification, shutdown_*, etc.)
|
||||||
if (shouldSuppressDesktopNotificationForInboxText(msg.text)) continue;
|
if (shouldSuppressDesktopNotificationForInboxText(msg.text)) continue;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { classifyIdleNotificationText } from '@shared/utils/idleNotificationSemantics';
|
import { classifyIdleNotificationText } from '@shared/utils/idleNotificationSemantics';
|
||||||
import { createLogger } from '@shared/utils/logger';
|
import { createLogger } from '@shared/utils/logger';
|
||||||
import { buildStandaloneSlashCommandMeta } from '@shared/utils/slashCommands';
|
import { buildStandaloneSlashCommandMeta } from '@shared/utils/slashCommands';
|
||||||
import { isTeamInternalControlMessageText } from '@shared/utils/teamInternalControlMessages';
|
import { isTeamInternalControlMessageEnvelope } from '@shared/utils/teamInternalControlMessages';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
import { getEffectiveInboxMessageId } from './inboxMessageIdentity';
|
import { getEffectiveInboxMessageId } from './inboxMessageIdentity';
|
||||||
|
|
@ -140,7 +140,7 @@ function buildSyntheticOpenCodeBootstrapMessages(config: TeamConfig): InboxMessa
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVisibleTeamMessage(message: InboxMessage): boolean {
|
function isVisibleTeamMessage(message: InboxMessage): boolean {
|
||||||
return !isTeamInternalControlMessageText(message.text);
|
return !isTeamInternalControlMessageEnvelope(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
function annotateSlashCommandResponses(messages: InboxMessage[]): void {
|
function annotateSlashCommandResponses(messages: InboxMessage[]): void {
|
||||||
|
|
|
||||||
|
|
@ -84,11 +84,14 @@ import { createLogger } from '@shared/utils/logger';
|
||||||
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
import { migrateProviderBackendId } from '@shared/utils/providerBackend';
|
||||||
import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
|
import { isDefaultProviderModelSelection } from '@shared/utils/providerModelSelection';
|
||||||
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
import { formatTaskDisplayLabel } from '@shared/utils/taskIdentity';
|
||||||
|
import {
|
||||||
|
isTeamInternalControlMessageText,
|
||||||
|
stripExactInternalControlEchoPrefix,
|
||||||
|
} from '@shared/utils/teamInternalControlMessages';
|
||||||
import {
|
import {
|
||||||
parseAllTeammateMessages,
|
parseAllTeammateMessages,
|
||||||
type ParsedTeammateContent,
|
type ParsedTeammateContent,
|
||||||
} from '@shared/utils/teammateMessageParser';
|
} from '@shared/utils/teammateMessageParser';
|
||||||
import { isTeamInternalControlMessageText } from '@shared/utils/teamInternalControlMessages';
|
|
||||||
import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors';
|
import { buildTeamMemberColorMap } from '@shared/utils/teamMemberColors';
|
||||||
import { createCliAutoSuffixNameGuard, parseNumericSuffixName } from '@shared/utils/teamMemberName';
|
import { createCliAutoSuffixNameGuard, parseNumericSuffixName } from '@shared/utils/teamMemberName';
|
||||||
import {
|
import {
|
||||||
|
|
@ -143,6 +146,14 @@ import {
|
||||||
type TeamRuntimeSettingsJson,
|
type TeamRuntimeSettingsJson,
|
||||||
} from '../runtime/teamRuntimeSettingsBundle';
|
} from '../runtime/teamRuntimeSettingsBundle';
|
||||||
|
|
||||||
|
import {
|
||||||
|
parseBootstrapRuntimeProofDetail,
|
||||||
|
validateBootstrapRuntimeProofEnvelope,
|
||||||
|
} from './bootstrap/BootstrapProofValidation';
|
||||||
|
import {
|
||||||
|
buildNativeAppManagedBootstrapSpecs,
|
||||||
|
type NativeAppManagedBootstrapSpec,
|
||||||
|
} from './bootstrap/NativeAppManagedBootstrapContextBuilder';
|
||||||
import {
|
import {
|
||||||
createOpenCodePromptDeliveryLedgerStore,
|
createOpenCodePromptDeliveryLedgerStore,
|
||||||
hashOpenCodePromptDeliveryPayload,
|
hashOpenCodePromptDeliveryPayload,
|
||||||
|
|
@ -256,14 +267,6 @@ import {
|
||||||
import { TeamSentMessagesStore } from './TeamSentMessagesStore';
|
import { TeamSentMessagesStore } from './TeamSentMessagesStore';
|
||||||
import { TeamTaskReader } from './TeamTaskReader';
|
import { TeamTaskReader } from './TeamTaskReader';
|
||||||
import { TeamTranscriptProjectResolver } from './TeamTranscriptProjectResolver';
|
import { TeamTranscriptProjectResolver } from './TeamTranscriptProjectResolver';
|
||||||
import {
|
|
||||||
buildNativeAppManagedBootstrapSpecs,
|
|
||||||
type NativeAppManagedBootstrapSpec,
|
|
||||||
} from './bootstrap/NativeAppManagedBootstrapContextBuilder';
|
|
||||||
import {
|
|
||||||
parseBootstrapRuntimeProofDetail,
|
|
||||||
validateBootstrapRuntimeProofEnvelope,
|
|
||||||
} from './bootstrap/BootstrapProofValidation';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
OpenCodeCommittedBootstrapSessionRecord,
|
OpenCodeCommittedBootstrapSessionRecord,
|
||||||
|
|
@ -19399,7 +19402,12 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
// Strip agent-only blocks — lead may respond with pure coordination content
|
// Strip agent-only blocks — lead may respond with pure coordination content
|
||||||
// that is not meant for the human user.
|
// that is not meant for the human user.
|
||||||
const cleanReply = replyText ? stripAgentBlocks(replyText) : null;
|
const cleanReply = replyText
|
||||||
|
? stripExactInternalControlEchoPrefix(
|
||||||
|
stripAgentBlocks(replyText),
|
||||||
|
stripAgentBlocks(message)
|
||||||
|
)
|
||||||
|
: null;
|
||||||
if (cleanReply) {
|
if (cleanReply) {
|
||||||
if (isTeamInternalControlMessageText(cleanReply)) {
|
if (isTeamInternalControlMessageText(cleanReply)) {
|
||||||
logger.debug(`[${teamName}] Suppressed internal lead relay echo`);
|
logger.debug(`[${teamName}] Suppressed internal lead relay echo`);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
|
import { getClaudeBasePath } from '@main/utils/pathDecoder';
|
||||||
|
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
||||||
import * as agentTeamsControllerModule from 'agent-teams-controller';
|
import * as agentTeamsControllerModule from 'agent-teams-controller';
|
||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
import { getClaudeBasePath } from '@main/utils/pathDecoder';
|
|
||||||
import type { TeamCreateRequest, TeamProviderId } from '@shared/types';
|
import type { TeamCreateRequest, TeamProviderId } from '@shared/types';
|
||||||
import { normalizeOptionalTeamProviderId } from '@shared/utils/teamProvider';
|
|
||||||
|
|
||||||
const { createController } = agentTeamsControllerModule;
|
const { createController } = agentTeamsControllerModule;
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ function redactNativeBootstrapContextText(input: string): string {
|
||||||
.replace(/sk-ant-[A-Za-z0-9_-]+/g, '[REDACTED_ANTHROPIC_API_KEY]')
|
.replace(/sk-ant-[A-Za-z0-9_-]+/g, '[REDACTED_ANTHROPIC_API_KEY]')
|
||||||
.replace(/sk-[A-Za-z0-9_-]{20,}/g, '[REDACTED_API_KEY]')
|
.replace(/sk-[A-Za-z0-9_-]{20,}/g, '[REDACTED_API_KEY]')
|
||||||
.replace(/(ANTHROPIC_API_KEY|OPENAI_API_KEY|CODEX_API_KEY)=\S+/g, '$1=[REDACTED]')
|
.replace(/(ANTHROPIC_API_KEY|OPENAI_API_KEY|CODEX_API_KEY)=\S+/g, '$1=[REDACTED]')
|
||||||
.replace(/Bearer\s+[A-Za-z0-9._-]+/gi, 'Bearer [REDACTED]');
|
.replace(/Bearer\s+[A-Z0-9._-]+/gi, 'Bearer [REDACTED]');
|
||||||
}
|
}
|
||||||
|
|
||||||
function boundText(input: string, maxChars: number): string {
|
function boundText(input: string, maxChars: number): string {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import {
|
||||||
} from '@renderer/utils/teamModelCatalog';
|
} from '@renderer/utils/teamModelCatalog';
|
||||||
import {
|
import {
|
||||||
isNativeAppManagedBootstrapCheckText,
|
isNativeAppManagedBootstrapCheckText,
|
||||||
isTeamInternalControlMessageText,
|
isTeamInternalControlMessageEnvelope,
|
||||||
} from '@shared/utils/teamInternalControlMessages';
|
} from '@shared/utils/teamInternalControlMessages';
|
||||||
|
|
||||||
import type { InboxMessage, TeamProviderId } from '@shared/types';
|
import type { InboxMessage, TeamProviderId } from '@shared/types';
|
||||||
|
|
@ -135,17 +135,17 @@ export interface InternalControlMessageDisplay {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInternalControlMessageDisplay(
|
export function getInternalControlMessageDisplay(
|
||||||
message: Pick<InboxMessage, 'text'>
|
message: Pick<InboxMessage, 'text'> & Partial<Pick<InboxMessage, 'source'>>
|
||||||
): InternalControlMessageDisplay | null {
|
): InternalControlMessageDisplay | null {
|
||||||
|
if (!isTeamInternalControlMessageEnvelope(message)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (isNativeAppManagedBootstrapCheckText(message.text)) {
|
if (isNativeAppManagedBootstrapCheckText(message.text)) {
|
||||||
return {
|
return {
|
||||||
summary: 'Internal bootstrap check',
|
summary: 'Internal bootstrap check',
|
||||||
body: 'Internal bootstrap check hidden in the UI.',
|
body: 'Internal bootstrap check hidden in the UI.',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (!isTeamInternalControlMessageText(message.text)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
summary: 'Internal control message',
|
summary: 'Internal control message',
|
||||||
body: 'Internal control message hidden in the UI.',
|
body: 'Internal control message hidden in the UI.',
|
||||||
|
|
@ -236,7 +236,9 @@ export function getBootstrapAcknowledgementDisplay(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSanitizedInboxMessageText(message: Pick<InboxMessage, 'text' | 'to'>): string {
|
export function getSanitizedInboxMessageText(
|
||||||
|
message: Pick<InboxMessage, 'text' | 'to'> & Partial<Pick<InboxMessage, 'source'>>
|
||||||
|
): string {
|
||||||
return (
|
return (
|
||||||
getInternalControlMessageDisplay(message)?.body ??
|
getInternalControlMessageDisplay(message)?.body ??
|
||||||
getBootstrapPromptDisplay(message)?.body ??
|
getBootstrapPromptDisplay(message)?.body ??
|
||||||
|
|
@ -247,7 +249,8 @@ export function getSanitizedInboxMessageText(message: Pick<InboxMessage, 'text'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSanitizedInboxMessageSummary(
|
export function getSanitizedInboxMessageSummary(
|
||||||
message: Pick<InboxMessage, 'text' | 'to' | 'from' | 'summary'>
|
message: Pick<InboxMessage, 'text' | 'to' | 'from' | 'summary'> &
|
||||||
|
Partial<Pick<InboxMessage, 'source'>>
|
||||||
): string {
|
): string {
|
||||||
return (
|
return (
|
||||||
getInternalControlMessageDisplay(message)?.summary ??
|
getInternalControlMessageDisplay(message)?.summary ??
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import {
|
||||||
} from '@renderer/utils/bootstrapPromptSanitizer';
|
} from '@renderer/utils/bootstrapPromptSanitizer';
|
||||||
import { shouldKeepIdleMessageInActivityWhenNoiseHidden } from '@renderer/utils/idleNotificationSemantics';
|
import { shouldKeepIdleMessageInActivityWhenNoiseHidden } from '@renderer/utils/idleNotificationSemantics';
|
||||||
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
|
import { isInboxNoiseMessage } from '@shared/utils/inboxNoise';
|
||||||
import { isTeamInternalControlMessageText } from '@shared/utils/teamInternalControlMessages';
|
import { isTeamInternalControlMessageEnvelope } from '@shared/utils/teamInternalControlMessages';
|
||||||
|
|
||||||
import type { InboxMessage } from '@shared/types';
|
import type { InboxMessage } from '@shared/types';
|
||||||
|
|
||||||
|
|
@ -127,8 +127,7 @@ export function filterTeamMessages(
|
||||||
const leadNames = normalizeLeadNames(rawLeadNames);
|
const leadNames = normalizeLeadNames(rawLeadNames);
|
||||||
|
|
||||||
let list = messages.filter(
|
let list = messages.filter(
|
||||||
(m) =>
|
(m) => m.messageKind !== 'task_comment_notification' && !isTeamInternalControlMessageEnvelope(m)
|
||||||
m.messageKind !== 'task_comment_notification' && !isTeamInternalControlMessageText(m.text)
|
|
||||||
);
|
);
|
||||||
if (timeWindow) {
|
if (timeWindow) {
|
||||||
list = list.filter((m) => {
|
list = list.filter((m) => {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,14 @@ const NATIVE_APP_MANAGED_BOOTSTRAP_CHECK_OPEN = '<agent_teams_native_app_managed
|
||||||
const LEAD_INBOX_RELAY_PROMPT_OPEN = 'You have new inbox messages addressed to you (team lead ';
|
const LEAD_INBOX_RELAY_PROMPT_OPEN = 'You have new inbox messages addressed to you (team lead ';
|
||||||
const TEAMMATE_MESSAGE_OPEN_RE = /^<teammate-message\s/i;
|
const TEAMMATE_MESSAGE_OPEN_RE = /^<teammate-message\s/i;
|
||||||
|
|
||||||
function stripTranscriptSpeakerPrefix(value: string): string {
|
const INTERNAL_CONTROL_MESSAGE_SOURCES = new Set([
|
||||||
|
'lead_process',
|
||||||
|
'lead_session',
|
||||||
|
'runtime_delivery',
|
||||||
|
'system_notification',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function stripTranscriptSpeakerPrefix(value: string): string {
|
||||||
let normalized = value.trim();
|
let normalized = value.trim();
|
||||||
for (let i = 0; i < 3; i += 1) {
|
for (let i = 0; i < 3; i += 1) {
|
||||||
const next = normalized.replace(/^(?:Human|User):\s*/i, '').trimStart();
|
const next = normalized.replace(/^(?:Human|User):\s*/i, '').trimStart();
|
||||||
|
|
@ -45,3 +52,25 @@ export function isTeamInternalControlMessageText(value: unknown): boolean {
|
||||||
isTeammateProtocolControlText(value)
|
isTeammateProtocolControlText(value)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isTeamInternalControlMessageEnvelope(message: {
|
||||||
|
text?: unknown;
|
||||||
|
source?: unknown;
|
||||||
|
}): boolean {
|
||||||
|
if (!isTeamInternalControlMessageText(message.text)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return typeof message.source === 'string' && INTERNAL_CONTROL_MESSAGE_SOURCES.has(message.source);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripExactInternalControlEchoPrefix(
|
||||||
|
value: string,
|
||||||
|
expectedControlText: string
|
||||||
|
): string {
|
||||||
|
const text = stripTranscriptSpeakerPrefix(value);
|
||||||
|
const expected = stripTranscriptSpeakerPrefix(expectedControlText);
|
||||||
|
if (!expected || !text.startsWith(expected)) {
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
return text.slice(expected.length).trim();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,32 @@ describe('TeamMessageFeedService', () => {
|
||||||
expect(feed.messages.map((message) => message.messageId)).toEqual(['visible-user-message']);
|
expect(feed.messages.map((message) => message.messageId)).toEqual(['visible-user-message']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not hide user-authored text just because it resembles an internal prompt', async () => {
|
||||||
|
const service = new TeamMessageFeedService({
|
||||||
|
getConfig: vi.fn(async () => config),
|
||||||
|
getInboxMessages: vi.fn(async () => [
|
||||||
|
makeMessage({
|
||||||
|
messageId: 'quoted-control-prompt',
|
||||||
|
source: 'user_sent',
|
||||||
|
text: `Human: You have new inbox messages addressed to you (team lead "team-lead").
|
||||||
|
Process them in order (oldest first).
|
||||||
|
|
||||||
|
Messages:
|
||||||
|
1) From: tom
|
||||||
|
Timestamp: 2026-05-06T15:02:54.853Z
|
||||||
|
Text:
|
||||||
|
#f8d7235a done.`,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
getLeadSessionMessages: vi.fn(async () => []),
|
||||||
|
getSentMessages: vi.fn(async () => []),
|
||||||
|
});
|
||||||
|
|
||||||
|
const feed = await service.getFeed('signal-ops-4');
|
||||||
|
|
||||||
|
expect(feed.messages.map((message) => message.messageId)).toEqual(['quoted-control-prompt']);
|
||||||
|
});
|
||||||
|
|
||||||
it('refreshes the durable feed after cache expiry even when the dirty signal was missed', async () => {
|
it('refreshes the durable feed after cache expiry even when the dirty signal was missed', async () => {
|
||||||
let inboxMessages: InboxMessage[] = [makeMessage()];
|
let inboxMessages: InboxMessage[] = [makeMessage()];
|
||||||
const getInboxMessages = vi.fn(async () => inboxMessages);
|
const getInboxMessages = vi.fn(async () => inboxMessages);
|
||||||
|
|
|
||||||
|
|
@ -342,6 +342,47 @@ describe('TeamProvisioningService relayLeadInboxMessages', () => {
|
||||||
expect(hoisted.files.get(`/mock/teams/${teamName}/sentMessages.json`)).toBeUndefined();
|
expect(hoisted.files.get(`/mock/teams/${teamName}/sentMessages.json`)).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('preserves visible summary text after stripping an echoed lead relay prompt', async () => {
|
||||||
|
const service = new TeamProvisioningService();
|
||||||
|
const teamName = 'my-team';
|
||||||
|
seedConfig(teamName);
|
||||||
|
seedLeadInbox(teamName, [
|
||||||
|
{
|
||||||
|
from: 'tom',
|
||||||
|
text: '#f8d7235a done.',
|
||||||
|
timestamp: '2026-02-23T10:00:00.000Z',
|
||||||
|
read: false,
|
||||||
|
summary: '#f8d7235a done',
|
||||||
|
messageId: 'm-1',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const { writeSpy } = attachAliveRun(service, teamName);
|
||||||
|
const relayPromise = service.relayLeadInboxMessages(teamName);
|
||||||
|
const run = await waitForCapture(service);
|
||||||
|
const payload = JSON.parse(String(writeSpy.mock.calls[0]?.[0] ?? '{}')) as {
|
||||||
|
message?: { content?: Array<{ text?: string }> };
|
||||||
|
};
|
||||||
|
const relayedPrompt = payload.message?.content?.[0]?.text ?? '';
|
||||||
|
|
||||||
|
(service as any).handleStreamJsonMessage(run, {
|
||||||
|
type: 'assistant',
|
||||||
|
content: [{ type: 'text', text: `Human: ${relayedPrompt}\n\nDelegated to bob.` }],
|
||||||
|
});
|
||||||
|
(service as any).handleStreamJsonMessage(run, { type: 'result', subtype: 'success' });
|
||||||
|
|
||||||
|
await expect(relayPromise).resolves.toBe(1);
|
||||||
|
expect(service.getLiveLeadProcessMessages(teamName).map((message) => message.text)).toEqual([
|
||||||
|
'Delegated to bob.',
|
||||||
|
]);
|
||||||
|
const sentRows = JSON.parse(
|
||||||
|
hoisted.files.get(`/mock/teams/${teamName}/sentMessages.json`) ?? '[]'
|
||||||
|
) as Array<{
|
||||||
|
text?: string;
|
||||||
|
}>;
|
||||||
|
expect(sentRows.map((message) => message.text)).toEqual(['Delegated to bob.']);
|
||||||
|
});
|
||||||
|
|
||||||
it('treats member work sync nudges as actionable in lead relay prompt', async () => {
|
it('treats member work sync nudges as actionable in lead relay prompt', async () => {
|
||||||
const service = new TeamProvisioningService();
|
const service = new TeamProvisioningService();
|
||||||
const teamName = 'my-team';
|
const teamName = 'my-team';
|
||||||
|
|
|
||||||
|
|
@ -67,16 +67,20 @@ Do NOT send acknowledgement-only messages such as "ready" or "online".`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sanitizes native app-managed bootstrap private control prompts defensively', () => {
|
it('sanitizes native app-managed bootstrap private control prompts defensively', () => {
|
||||||
const message = makeMessage(`<agent_teams_native_app_managed_bootstrap_check>
|
const message = makeMessage(
|
||||||
|
`<agent_teams_native_app_managed_bootstrap_check>
|
||||||
Your Agent Teams startup context was already loaded by the app.
|
Your Agent Teams startup context was already loaded by the app.
|
||||||
</agent_teams_native_app_managed_bootstrap_check>`);
|
</agent_teams_native_app_managed_bootstrap_check>`,
|
||||||
|
{ source: 'system_notification' }
|
||||||
|
);
|
||||||
|
|
||||||
expect(getInternalControlMessageDisplay(message)?.summary).toBe('Internal bootstrap check');
|
expect(getInternalControlMessageDisplay(message)?.summary).toBe('Internal bootstrap check');
|
||||||
expect(getSanitizedInboxMessageText(message)).toBe('Internal bootstrap check hidden in the UI.');
|
expect(getSanitizedInboxMessageText(message)).toBe('Internal bootstrap check hidden in the UI.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sanitizes leaked lead inbox relay prompts defensively', () => {
|
it('sanitizes leaked lead inbox relay prompts defensively', () => {
|
||||||
const message = makeMessage(`Human: You have new inbox messages addressed to you (team lead "team-lead").
|
const message = makeMessage(
|
||||||
|
`Human: You have new inbox messages addressed to you (team lead "team-lead").
|
||||||
Process them in order (oldest first).
|
Process them in order (oldest first).
|
||||||
If action is required, delegate via task creation or SendMessage, and keep responses minimal.
|
If action is required, delegate via task creation or SendMessage, and keep responses minimal.
|
||||||
|
|
||||||
|
|
@ -84,9 +88,26 @@ Messages:
|
||||||
1) From: tom
|
1) From: tom
|
||||||
Timestamp: 2026-05-06T15:02:54.853Z
|
Timestamp: 2026-05-06T15:02:54.853Z
|
||||||
Text:
|
Text:
|
||||||
#f8d7235a done.`);
|
#f8d7235a done.`,
|
||||||
|
{ source: 'lead_process' }
|
||||||
|
);
|
||||||
|
|
||||||
expect(getInternalControlMessageDisplay(message)?.summary).toBe('Internal control message');
|
expect(getInternalControlMessageDisplay(message)?.summary).toBe('Internal control message');
|
||||||
expect(getSanitizedInboxMessageText(message)).toBe('Internal control message hidden in the UI.');
|
expect(getSanitizedInboxMessageText(message)).toBe('Internal control message hidden in the UI.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not sanitize user-authored text that quotes an internal prompt', () => {
|
||||||
|
const text = `Human: You have new inbox messages addressed to you (team lead "team-lead").
|
||||||
|
Process them in order (oldest first).
|
||||||
|
|
||||||
|
Messages:
|
||||||
|
1) From: tom
|
||||||
|
Timestamp: 2026-05-06T15:02:54.853Z
|
||||||
|
Text:
|
||||||
|
#f8d7235a done.`;
|
||||||
|
const message = makeMessage(text, { source: 'user_sent' });
|
||||||
|
|
||||||
|
expect(getInternalControlMessageDisplay(message)).toBeNull();
|
||||||
|
expect(getSanitizedInboxMessageText(message)).toBe(text);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -90,6 +90,31 @@ Messages:
|
||||||
expect(result.map((message) => message.messageId)).toEqual(['visible-message']);
|
expect(result.map((message) => message.messageId)).toEqual(['visible-message']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('does not hide user-authored text that quotes an internal prompt', () => {
|
||||||
|
const messages = [
|
||||||
|
makeMessage({
|
||||||
|
messageId: 'quoted-control-prompt',
|
||||||
|
source: 'user_sent',
|
||||||
|
text: `Human: You have new inbox messages addressed to you (team lead "team-lead").
|
||||||
|
Process them in order (oldest first).
|
||||||
|
|
||||||
|
Messages:
|
||||||
|
1) From: tom
|
||||||
|
Timestamp: 2026-05-06T15:02:54.853Z
|
||||||
|
Text:
|
||||||
|
#f8d7235a done.`,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = filterTeamMessages(messages, {
|
||||||
|
timeWindow: null,
|
||||||
|
filter: { from: new Set(), to: new Set(), showNoise: true },
|
||||||
|
searchQuery: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.map((message) => message.messageId)).toEqual(['quoted-control-prompt']);
|
||||||
|
});
|
||||||
|
|
||||||
it('hides Human-prefixed teammate protocol echoes', () => {
|
it('hides Human-prefixed teammate protocol echoes', () => {
|
||||||
const messages = [
|
const messages = [
|
||||||
makeMessage({
|
makeMessage({
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
isTeamInternalControlMessageEnvelope,
|
||||||
isLeadInboxRelayControlPromptText,
|
isLeadInboxRelayControlPromptText,
|
||||||
isTeamInternalControlMessageText,
|
isTeamInternalControlMessageText,
|
||||||
isTeammateProtocolControlText,
|
isTeammateProtocolControlText,
|
||||||
|
stripExactInternalControlEchoPrefix,
|
||||||
} from '@shared/utils/teamInternalControlMessages';
|
} from '@shared/utils/teamInternalControlMessages';
|
||||||
|
|
||||||
const leadRelayPrompt = `You have new inbox messages addressed to you (team lead "team-lead").
|
const leadRelayPrompt = `You have new inbox messages addressed to you (team lead "team-lead").
|
||||||
|
|
@ -39,4 +41,36 @@ describe('teamInternalControlMessages', () => {
|
||||||
expect(isTeammateProtocolControlText(text)).toBe(true);
|
expect(isTeammateProtocolControlText(text)).toBe(true);
|
||||||
expect(isTeamInternalControlMessageText(text)).toBe(true);
|
expect(isTeamInternalControlMessageText(text)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('only treats internal-looking text as hidden for internal message sources', () => {
|
||||||
|
expect(
|
||||||
|
isTeamInternalControlMessageEnvelope({
|
||||||
|
source: 'lead_process',
|
||||||
|
text: `Human: ${leadRelayPrompt}`,
|
||||||
|
})
|
||||||
|
).toBe(true);
|
||||||
|
expect(
|
||||||
|
isTeamInternalControlMessageEnvelope({
|
||||||
|
source: 'user_sent',
|
||||||
|
text: `Human: ${leadRelayPrompt}`,
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
expect(
|
||||||
|
isTeamInternalControlMessageEnvelope({
|
||||||
|
text: `Human: ${leadRelayPrompt}`,
|
||||||
|
})
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('strips an exact echoed control prefix while preserving visible trailing text', () => {
|
||||||
|
expect(stripExactInternalControlEchoPrefix(`Human: ${leadRelayPrompt}`, leadRelayPrompt)).toBe(
|
||||||
|
''
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
stripExactInternalControlEchoPrefix(
|
||||||
|
`Human: ${leadRelayPrompt}\n\nDelegated to bob.`,
|
||||||
|
leadRelayPrompt
|
||||||
|
)
|
||||||
|
).toBe('Delegated to bob.');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue