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
5 changes: 5 additions & 0 deletions .changeset/ten-ghosts-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@agentuity/sdk": patch
---

Add support for context.waitUntil to be able to run background processing without blocking the response
4 changes: 3 additions & 1 deletion src/apis/keyvalue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ export default class KeyValueAPI implements KeyValueStorage {
}
if (resp.status === 200) {
span.addEvent('hit');
let body: Buffer = Buffer.from(await resp.response.arrayBuffer() as ArrayBuffer);
let body: Buffer = Buffer.from(
(await resp.response.arrayBuffer()) as ArrayBuffer
);
if (resp.headers.get('content-encoding') === 'gzip') {
body = await gunzipBuffer(body);
}
Expand Down
18 changes: 18 additions & 0 deletions src/apis/session.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { POST } from './api';

/**
* Mark the current session as completed and pass the duration of the async execution in milliseconds.
*/
export async function markSessionCompleted(
sessionId: string,
duration: number
): Promise<void> {
const resp = await POST<void>(
'/agent/2025-03-17/session-completed',
JSON.stringify({ sessionId, duration })
);
if (resp.status === 202) {
return;
}
throw new Error(await resp.response.text());
}
2 changes: 1 addition & 1 deletion src/autostart/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export async function run(config: AutostartConfig) {
process.exit(1);
}
const ymlData = readFileSync(ymlfile, 'utf8').toString();
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
// biome-ignore lint/suspicious/noExplicitAny: yml.load requires any type
const data = yml.load(ymlData) as any;
if (!config.projectId && data?.project_id) {
config.projectId = data.project_id;
Expand Down
75 changes: 42 additions & 33 deletions src/io/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,23 @@ import type {
*/
function isPrivateIPv4(octets: number[]): boolean {
if (octets.length !== 4) return false;

const [a, b] = octets;

if (a === 10) return true;

if (a === 172 && b >= 16 && b <= 31) return true;

if (a === 192 && b === 168) return true;

if (a === 100 && b >= 64 && b <= 127) return true;

if (a === 169 && b === 254) return true;

if (a === 127) return true;

if (a === 0) return true;

return false;
}

Expand All @@ -48,20 +48,25 @@ function isPrivateIPv4(octets: number[]): boolean {
*/
function isBlockedIPv6(addr: string): boolean {
let normalized = addr.toLowerCase().trim();

if (normalized.startsWith('[') && normalized.endsWith(']')) {
normalized = normalized.slice(1, -1);
}

if (normalized === '::1') return true;

if (normalized === '::') return true;

if (normalized.startsWith('fe8') || normalized.startsWith('fe9') ||
normalized.startsWith('fea') || normalized.startsWith('feb')) return true;


if (
normalized.startsWith('fe8') ||
normalized.startsWith('fe9') ||
normalized.startsWith('fea') ||
normalized.startsWith('feb')
)
return true;

if (normalized.startsWith('fc') || normalized.startsWith('fd')) return true;

if (normalized.startsWith('::ffff:')) {
const ipv4Part = normalized.substring(7);
const ipv4Match = ipv4Part.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
Expand All @@ -77,19 +82,21 @@ function isBlockedIPv6(addr: string): boolean {
(high >> 8) & 0xff,
high & 0xff,
(low >> 8) & 0xff,
low & 0xff
low & 0xff,
];
return isPrivateIPv4(octets);
}
}

return false;
}

/**
* Check if hostname resolves to private or local addresses
*/
async function isResolvableToPrivateOrLocal(hostname: string): Promise<boolean> {
async function isResolvableToPrivateOrLocal(
hostname: string
): Promise<boolean> {
const ipVersion = isIP(hostname);
if (ipVersion === 4) {
const octets = hostname.split('.').map(Number);
Expand All @@ -98,10 +105,10 @@ async function isResolvableToPrivateOrLocal(hostname: string): Promise<boolean>
if (ipVersion === 6) {
return isBlockedIPv6(hostname);
}

try {
const result = await dns.lookup(hostname, { all: true, verbatim: true });

for (const { address, family } of result) {
if (family === 4) {
const octets = address.split('.').map(Number);
Expand All @@ -110,7 +117,7 @@ async function isResolvableToPrivateOrLocal(hostname: string): Promise<boolean>
if (isBlockedIPv6(address)) return true;
}
}

return false;
} catch {
return false;
Expand Down Expand Up @@ -185,12 +192,14 @@ class RemoteEmailAttachment implements IncomingEmailAttachment {
return await context.with(spanContext, async () => {
const parsed = new URL(this._url);
const hostname = parsed.hostname.toLowerCase().trim();

const isPrivateOrLocal = await isResolvableToPrivateOrLocal(hostname);
if (isPrivateOrLocal) {
throw new Error('Access to private or local network addresses is not allowed');
throw new Error(
'Access to private or local network addresses is not allowed'
);
}

const res = await send({ url: this._url, method: 'GET' }, true);
if (res.status === 200) {
span.setStatus({ code: SpanStatusCode.OK });
Expand Down Expand Up @@ -367,7 +376,7 @@ export class Email {
return [];
}
const validAttachments: IncomingEmailAttachment[] = [];

for (const att of this._message.attachments) {
const hv = att.headers.get('content-disposition') as {
value: string;
Expand All @@ -378,7 +387,7 @@ export class Email {
'Invalid attachment headers: missing content-disposition'
);
}

const filename =
hv.params.filename ??
hv.params['filename*'] ??
Expand All @@ -403,19 +412,19 @@ export class Email {
continue;
}
const hostname = parsed.hostname.toLowerCase().trim();

if (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '::1'
) {
continue;
}

if (isBlockedIPv6(hostname)) {
continue;
}

// Check for IPv4 addresses
const ipVersion = isIP(hostname);
if (ipVersion === 4) {
Expand All @@ -432,7 +441,7 @@ export class Email {
new RemoteEmailAttachment(filename, parsed.toString(), disposition)
);
}

return validAttachments;
}

Expand Down Expand Up @@ -468,7 +477,7 @@ export class Email {
'email authorization token is required but not found in metadata'
);
}
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: <explanation>
// biome-ignore lint/suspicious/noAsyncPromiseExecutor: needed for complex async email operations
return new Promise<string>(async (resolve, reject) => {
try {
let attachments: Attachment[] = [];
Expand Down
14 changes: 4 additions & 10 deletions src/io/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,9 +425,9 @@ export interface SlackReply {

export function isSlackEventPayload(data: unknown): data is SlackEventPayload {
if (typeof data !== 'object' || data === null) return false;

const obj = data as Record<string, unknown>;

return (
'token' in data &&
'team_id' in data &&
Expand Down Expand Up @@ -500,20 +500,14 @@ export class Slack implements SlackService {
// Execute the operation within the new context
return await context.with(spanContext, async () => {
span.setAttribute('@agentuity/agentId', ctx.agent.id);
span.setAttribute(
'@agentuity/slackEventType',
this.eventPayload.type
);
span.setAttribute('@agentuity/slackEventType', this.eventPayload.type);
if (
this.eventPayload.type !== 'event_callback' ||
!isSlackMessageEvent(this.eventPayload.event)
) {
throw new UnsupportedSlackPayload('Payload is not Slack message');
}
span.setAttribute(
'@agentuity/slackTeamId',
this.eventPayload.team_id
);
span.setAttribute('@agentuity/slackTeamId', this.eventPayload.team_id);

// Create payload matching backend structure
let payload: SlackReply;
Expand Down
2 changes: 1 addition & 1 deletion src/io/teams/AgentuityTeamsActivityHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export type AgentuityTeamsActivityHandlerConstructor = new (
ctx: AgentContext
) => AgentuityTeamsActivityHandler;

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
// biome-ignore lint/suspicious/noExplicitAny: prototype chain inspection requires any
const checkPrototypeChain = (prototype: any): boolean => {
let current = prototype;
while (current) {
Expand Down
10 changes: 5 additions & 5 deletions src/io/teams/AgentuityTeamsAdapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export class AgentuityTeamsAdapter {
MicrosoftAppTenantId: tenantId,
MicrosoftAppPassword: appPassword,
MicrosoftAppType: appType,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
// biome-ignore lint/suspicious/noExplicitAny: BotFrameworkAdapter config typing
} as any);
const provider = HandlerParameterProvider.getInstance();
this.adapter = new CloudAdapter(auth);
Expand All @@ -82,9 +82,9 @@ export class AgentuityTeamsAdapter {

async process() {
try {
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
// biome-ignore lint/suspicious/noExplicitAny: Teams payload structure varies
const teamsPayload = (await this.req.data.json()) as any;
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
// biome-ignore lint/suspicious/noExplicitAny: mock Restify request object
const mockRestifyReq: any = {
method: 'POST',
body: teamsPayload,
Expand All @@ -94,7 +94,7 @@ export class AgentuityTeamsAdapter {
: this.req.metadata.headers,
};

// biome-ignore lint/suspicious/noExplicitAny: <explanation>
// biome-ignore lint/suspicious/noExplicitAny: mock Restify response object
const mockRestifyRes: any = {
status: (_code: number) => {
return {
Expand All @@ -109,7 +109,7 @@ export class AgentuityTeamsAdapter {
await this.adapter.process(
mockRestifyReq,
mockRestifyRes,
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
// biome-ignore lint/suspicious/noExplicitAny: Bot Framework TurnContext typing
async (context: any) => {
const res = await this.bot.run(context);
return res;
Expand Down
6 changes: 3 additions & 3 deletions src/io/teams/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@ import { SimpleAgentuityTeamsBot } from './SimpleAgentuityTeamsBot';
type Mode = 'dev' | 'cloud';
type parseConfigResult = {
config: Record<string, string>;
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
// biome-ignore lint/suspicious/noExplicitAny: Teams payload can contain various data types
justPayload: Record<string, any>;
mode: Mode;
};

const parseConfig = (
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
// biome-ignore lint/suspicious/noExplicitAny: Teams payload structure is dynamic
payload: any,
metadata: JsonObject
): parseConfigResult => {
const keys = Object.keys(metadata);
let config: Record<string, string>;
let mode: Mode;
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
// biome-ignore lint/suspicious/noExplicitAny: payload content varies by Teams event type
let justPayload: Record<string, any>;

if (
Expand Down
2 changes: 1 addition & 1 deletion src/logger/console.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default class ConsoleLogger implements Logger {
? Object.entries(this.context)
.map(([key, value]) => {
try {
return `${key}=${typeof value === 'object' ? safeStringify(value) : value}`;
return `${key}=${typeof value === 'object' ? safeStringify(value) : value}`;
} catch (_err) {
return `${key}=[object Object]`;
}
Expand Down
9 changes: 6 additions & 3 deletions src/otel/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ export function registerOtel(config: OtelConfig): OtelResponse {
});
instrumentationSDK.start();
hostMetrics?.start();

try {
const projectName = config.projectId || '';
const orgName = config.orgId || '';
Expand All @@ -238,7 +238,7 @@ export function registerOtel(config: OtelConfig): OtelResponse {

initialize({
appName,
baseUrl: url,
baseUrl: url,
headers: traceloopHeaders,
disableBatch: devmode,
tracingEnabled: false, // Disable traceloop's own tracing (equivalent to Python's telemetryEnabled: false)
Expand All @@ -247,7 +247,10 @@ export function registerOtel(config: OtelConfig): OtelResponse {
logger.debug(`Traceloop initialized with app_name: ${appName}`);
logger.info('Traceloop configured successfully');
} catch (error) {
logger.warn('Traceloop not available, skipping automatic instrumentation', { error: error instanceof Error ? error.message : String(error) });
logger.warn(
'Traceloop not available, skipping automatic instrumentation',
{ error: error instanceof Error ? error.message : String(error) }
);
}
running = true;
}
Expand Down
4 changes: 2 additions & 2 deletions src/otel/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class OtelLogger implements Logger {
return message;
}
try {
return safeStringify(message);
return safeStringify(message);
} catch (_err) {
// Handle circular references or other JSON stringification errors
return String(message);
Expand Down Expand Up @@ -231,6 +231,6 @@ export function patchConsole(
delegate.debug('profileEnd:', ...args);
};

// biome-ignore lint/suspicious/noGlobalAssign: <explanation>
// biome-ignore lint/suspicious/noGlobalAssign: patching console for logging integration
console = globalThis.console = _patch;
}
Loading
Loading