From 29b6522c9b16c7366b1eaf68bd89a4293eca95aa Mon Sep 17 00:00:00 2001 From: Jesse Turner <57651174+jesseturner21@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:17:45 -0400 Subject: [PATCH 01/45] fix(import): use GatewayNameSchema for gateway import name validation (#1011) The import gateway command used NAME_REGEX which only allowed underscores and max 48 chars, rejecting valid gateway names with hyphens like "agentcore-gateway". Switch to GatewayNameSchema which matches the actual AWS API: alphanumeric with hyphens, up to 100 chars. Constraint: AWS CreateGateway API allows [0-9a-zA-Z] with hyphens Rejected: Updating NAME_REGEX | it is shared with other import commands that have different naming rules Confidence: high Scope-risk: narrow --- .../commands/import/__tests__/import-gateway-flow.test.ts | 5 ++--- src/cli/commands/import/import-gateway.ts | 8 +++++--- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/cli/commands/import/__tests__/import-gateway-flow.test.ts b/src/cli/commands/import/__tests__/import-gateway-flow.test.ts index 5b322e942..183a6e63f 100644 --- a/src/cli/commands/import/__tests__/import-gateway-flow.test.ts +++ b/src/cli/commands/import/__tests__/import-gateway-flow.test.ts @@ -310,14 +310,13 @@ describe('handleImportGateway', () => { // ── Name validation ───────────────────────────────────────────────────── describe('Name validation', () => { - it('rejects invalid name starting with a number', async () => { - mockGetGatewayDetail.mockResolvedValue(makeGatewayDetail({ name: '123gateway' })); + it('rejects invalid name with special characters', async () => { + mockGetGatewayDetail.mockResolvedValue(makeGatewayDetail({ name: 'gateway_with_underscores!' })); const result = await handleImportGateway({ arn: GATEWAY_ARN }); expect(result.success).toBe(false); expect(result.error).toContain('Invalid name'); - expect(result.error).toContain('must start with a letter'); expect(mockConfigIOInstance.writeProjectSpec).not.toHaveBeenCalled(); }); diff --git a/src/cli/commands/import/import-gateway.ts b/src/cli/commands/import/import-gateway.ts index 491246438..3c2384e03 100644 --- a/src/cli/commands/import/import-gateway.ts +++ b/src/cli/commands/import/import-gateway.ts @@ -9,6 +9,7 @@ import type { GatewayPolicyEngineConfiguration, OutboundAuth, } from '../../../schema'; +import { GatewayNameSchema } from '../../../schema'; import type { GatewayDetail, GatewayTargetDetail } from '../../aws/agentcore-control'; import { getGatewayDetail, @@ -17,7 +18,7 @@ import { listAllGateways, } from '../../aws/agentcore-control'; import { isAccessDeniedError } from '../../errors'; -import { ANSI, NAME_REGEX } from './constants'; +import { ANSI } from './constants'; import { executeCdkImportPipeline } from './import-pipeline'; import { failResult, @@ -425,10 +426,11 @@ export async function handleImportGateway(options: ImportResourceOptions): Promi // 4. Validate name logger.startStep('Validate name'); let localName = options.name ?? gatewayDetail.name; - if (!NAME_REGEX.test(localName)) { + const nameResult = GatewayNameSchema.safeParse(localName); + if (!nameResult.success) { return failResult( logger, - `Invalid name "${localName}". Name must start with a letter and contain only letters, numbers, and underscores (max 48 chars).`, + `Invalid name "${localName}". ${nameResult.error.issues[0]?.message ?? 'Invalid gateway name'}`, 'gateway', localName ); From 76b07aaee08b1ac7c03e87cd22f02bb8aa41ce1b Mon Sep 17 00:00:00 2001 From: Avi Alpert <131792194+avi-alpert@users.noreply.github.com> Date: Wed, 29 Apr 2026 09:27:33 -0400 Subject: [PATCH 02/45] feat: add CloudWatch traces API for web UI (#997) --- src/cli/commands/dev/browser-mode.ts | 43 +++ .../__tests__/cloudwatch-traces.test.ts | 233 ++++++++++++++++ src/cli/operations/dev/web-ui/api-types.ts | 34 +++ .../dev/web-ui/handlers/cloudwatch-traces.ts | 166 +++++++++++ .../operations/dev/web-ui/handlers/index.ts | 1 + src/cli/operations/dev/web-ui/index.ts | 7 + src/cli/operations/dev/web-ui/web-server.ts | 33 +++ .../traces/__tests__/get-trace.test.ts | 233 ++++++++++++++++ .../traces/__tests__/list-traces.test.ts | 135 +++++++++ src/cli/operations/traces/get-trace.ts | 263 +++++++++++------- src/cli/operations/traces/index.ts | 16 +- src/cli/operations/traces/insights-query.ts | 85 ++++++ src/cli/operations/traces/list-traces.ts | 115 ++------ src/cli/operations/traces/types.ts | 77 +++++ 14 files changed, 1244 insertions(+), 197 deletions(-) create mode 100644 src/cli/operations/dev/web-ui/__tests__/cloudwatch-traces.test.ts create mode 100644 src/cli/operations/dev/web-ui/handlers/cloudwatch-traces.ts create mode 100644 src/cli/operations/traces/__tests__/get-trace.test.ts create mode 100644 src/cli/operations/traces/__tests__/list-traces.test.ts create mode 100644 src/cli/operations/traces/insights-query.ts create mode 100644 src/cli/operations/traces/types.ts diff --git a/src/cli/commands/dev/browser-mode.ts b/src/cli/commands/dev/browser-mode.ts index 3846dea85..c35113e6d 100644 --- a/src/cli/commands/dev/browser-mode.ts +++ b/src/cli/commands/dev/browser-mode.ts @@ -9,6 +9,8 @@ import { runWebUI, } from '../../operations/dev/web-ui'; import { listMemoryRecords, retrieveMemoryRecords } from '../../operations/memory'; +import { loadDeployedProjectConfig, resolveAgent } from '../../operations/resolve-agent'; +import { fetchTraceRecords, listTraces } from '../../operations/traces'; import path from 'node:path'; interface DeployedHandlers { @@ -192,6 +194,47 @@ export async function runBrowserMode(opts: BrowserModeOptions): Promise { ? (agentNameParam, startTime, endTime) => collector.listTraces(agentNameParam, startTime, endTime) : undefined, onGetTrace: collector ? (agentNameParam, traceId) => collector.getTraceSpans(agentNameParam, traceId) : undefined, + onListCloudWatchTraces: async (agentName, _harnessName, startTime, endTime) => { + try { + const configIO = new ConfigIO({ baseDir }); + const context = await loadDeployedProjectConfig(configIO); + const resolved = resolveAgent(context, { runtime: agentName }); + if (!resolved.success) return { success: false, error: resolved.error }; + return listTraces({ + region: resolved.agent.region, + runtimeId: resolved.agent.runtimeId, + agentName: resolved.agent.agentName, + startTime, + endTime, + }); + } catch (err) { + return { + success: false, + error: `Failed to list CloudWatch traces: ${err instanceof Error ? err.message : String(err)}`, + }; + } + }, + onGetCloudWatchTrace: async (agentName, _harnessName, traceId, startTime, endTime) => { + try { + const configIO = new ConfigIO({ baseDir }); + const context = await loadDeployedProjectConfig(configIO); + const resolved = resolveAgent(context, { runtime: agentName }); + if (!resolved.success) return { success: false, error: resolved.error }; + return fetchTraceRecords({ + region: resolved.agent.region, + runtimeId: resolved.agent.runtimeId, + traceId, + startTime, + endTime, + includeSpans: true, + }); + } catch (err) { + return { + success: false, + error: `Failed to get CloudWatch trace: ${err instanceof Error ? err.message : String(err)}`, + }; + } + }, onListMemoryRecords: async (memoryName, namespace, strategyId) => { const deployed = await resolveDeployedHandlers(baseDir, onLog); if (!deployed.onListMemoryRecords) return { success: false, error: 'No deployed AgentCore Memory found' }; diff --git a/src/cli/operations/dev/web-ui/__tests__/cloudwatch-traces.test.ts b/src/cli/operations/dev/web-ui/__tests__/cloudwatch-traces.test.ts new file mode 100644 index 000000000..f0210f63a --- /dev/null +++ b/src/cli/operations/dev/web-ui/__tests__/cloudwatch-traces.test.ts @@ -0,0 +1,233 @@ +import { handleGetCloudWatchTrace, handleListCloudWatchTraces } from '../handlers/cloudwatch-traces.js'; +import type { RouteContext } from '../handlers/route-context.js'; +import type { IncomingMessage, ServerResponse } from 'http'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +function mockRes(): ServerResponse & { _status: number; _headers: Record; _body: string } { + const res = { + _status: 0, + _headers: {} as Record, + _body: '', + writeHead(status: number, headers?: Record) { + res._status = status; + if (headers) Object.assign(res._headers, headers); + return res; + }, + setHeader(name: string, value: string) { + res._headers[name] = value; + }, + end(body?: string) { + if (body) res._body = body; + }, + }; + return res as unknown as ServerResponse & { _status: number; _headers: Record; _body: string }; +} + +function mockReq(url: string): IncomingMessage { + return { url, headers: { host: 'localhost:8081' } } as unknown as IncomingMessage; +} + +function mockCtx(overrides: Partial = {}): RouteContext { + return { + options: { + mode: 'dev', + agents: [], + harnesses: [], + uiPort: 8081, + ...overrides, + }, + runningAgents: new Map(), + startingAgents: new Map(), + agentErrors: new Map(), + setCorsHeaders: vi.fn(), + readBody: vi.fn(), + } as RouteContext; +} + +describe('handleListCloudWatchTraces', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('returns 404 when no handler configured', async () => { + const ctx = mockCtx(); + const req = mockReq('/api/cloudwatch-traces?agentName=my-agent'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(404); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('not available'); + }); + + it('returns 400 when neither agentName nor harnessName provided', async () => { + const handler = vi.fn(); + const ctx = mockCtx({ onListCloudWatchTraces: handler }); + const req = mockReq('/api/cloudwatch-traces'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(400); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('agentName'); + expect(body.error).toContain('harnessName'); + expect(handler).not.toHaveBeenCalled(); + }); + + it('returns 400 when both agentName and harnessName provided', async () => { + const handler = vi.fn(); + const ctx = mockCtx({ onListCloudWatchTraces: handler }); + const req = mockReq('/api/cloudwatch-traces?agentName=a&harnessName=h'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(400); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('agentName'); + expect(body.error).toContain('harnessName'); + expect(handler).not.toHaveBeenCalled(); + }); + + it('calls handler with agentName and returns traces', async () => { + const traces = [{ traceId: 't1' }, { traceId: 't2' }]; + const handler = vi.fn().mockResolvedValue({ success: true, traces }); + const ctx = mockCtx({ onListCloudWatchTraces: handler }); + const req = mockReq('/api/cloudwatch-traces?agentName=my-agent'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(200); + expect(handler).toHaveBeenCalledWith('my-agent', undefined, undefined, undefined); + const body = JSON.parse(res._body); + expect(body.success).toBe(true); + expect(body.traces).toEqual(traces); + }); + + it('calls handler with harnessName', async () => { + const handler = vi.fn().mockResolvedValue({ success: true, traces: [] }); + const ctx = mockCtx({ onListCloudWatchTraces: handler }); + const req = mockReq('/api/cloudwatch-traces?harnessName=my-harness'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(200); + expect(handler).toHaveBeenCalledWith(undefined, 'my-harness', undefined, undefined); + }); + + it('returns 500 when handler throws', async () => { + const handler = vi.fn().mockRejectedValue(new Error('boom')); + const ctx = mockCtx({ onListCloudWatchTraces: handler }); + const req = mockReq('/api/cloudwatch-traces?agentName=my-agent'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(500); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('Failed to list CloudWatch traces'); + }); + + it('returns 400 for invalid startTime', async () => { + const handler = vi.fn(); + const ctx = mockCtx({ onListCloudWatchTraces: handler }); + const req = mockReq('/api/cloudwatch-traces?agentName=my-agent&startTime=notanumber'); + const res = mockRes(); + + await handleListCloudWatchTraces(ctx, req, res); + + expect(res._status).toBe(400); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('startTime'); + expect(handler).not.toHaveBeenCalled(); + }); +}); + +describe('handleGetCloudWatchTrace', () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it('returns 404 when no handler configured', async () => { + const ctx = mockCtx(); + const req = mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent'); + const res = mockRes(); + + await handleGetCloudWatchTrace(ctx, req, res); + + expect(res._status).toBe(404); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('not available'); + }); + + it('returns 400 when traceId is missing', async () => { + const handler = vi.fn(); + const ctx = mockCtx({ onGetCloudWatchTrace: handler }); + const req = mockReq('/api/cloudwatch-traces/?agentName=my-agent'); + const res = mockRes(); + + await handleGetCloudWatchTrace(ctx, req, res); + + expect(res._status).toBe(400); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('traceId'); + expect(handler).not.toHaveBeenCalled(); + }); + + it('returns 400 when neither agentName nor harnessName provided', async () => { + const handler = vi.fn(); + const ctx = mockCtx({ onGetCloudWatchTrace: handler }); + const req = mockReq('/api/cloudwatch-traces/abc123'); + const res = mockRes(); + + await handleGetCloudWatchTrace(ctx, req, res); + + expect(res._status).toBe(400); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('agentName'); + expect(body.error).toContain('harnessName'); + expect(handler).not.toHaveBeenCalled(); + }); + + it('returns 500 when handler throws', async () => { + const handler = vi.fn().mockRejectedValue(new Error('boom')); + const ctx = mockCtx({ onGetCloudWatchTrace: handler }); + const req = mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent'); + const res = mockRes(); + + await handleGetCloudWatchTrace(ctx, req, res); + + expect(res._status).toBe(500); + const body = JSON.parse(res._body); + expect(body.success).toBe(false); + expect(body.error).toContain('Failed to get CloudWatch trace'); + }); + + it('calls handler and returns records', async () => { + const records = [{ record: 'data1' }]; + const handler = vi.fn().mockResolvedValue({ success: true, records }); + const ctx = mockCtx({ onGetCloudWatchTrace: handler }); + const req = mockReq('/api/cloudwatch-traces/abc123?agentName=my-agent'); + const res = mockRes(); + + await handleGetCloudWatchTrace(ctx, req, res); + + expect(res._status).toBe(200); + expect(handler).toHaveBeenCalledWith('my-agent', undefined, 'abc123', undefined, undefined); + const body = JSON.parse(res._body); + expect(body.success).toBe(true); + expect(body.records).toEqual(records); + }); +}); diff --git a/src/cli/operations/dev/web-ui/api-types.ts b/src/cli/operations/dev/web-ui/api-types.ts index 509d834ff..8ba57937e 100644 --- a/src/cli/operations/dev/web-ui/api-types.ts +++ b/src/cli/operations/dev/web-ui/api-types.ts @@ -8,6 +8,7 @@ * TODO: Extract these types into a shared package so both repos import * from a single source of truth instead of manually duplicating. */ +import type { CloudWatchSpanRecord, CloudWatchTraceRecord } from '../../traces/types'; // --------------------------------------------------------------------------- // GET /api/status @@ -279,6 +280,39 @@ export interface GetTraceResponse { error?: string; } +// --------------------------------------------------------------------------- +// GET /api/cloudwatch-traces?agentName=xxx|harnessName=xxx +// --------------------------------------------------------------------------- + +/** A single trace entry returned by the CloudWatch traces list endpoint */ +export interface CloudWatchTraceEntry { + traceId: string; + timestamp: string; + sessionId?: string; + spanCount?: string; +} + +/** Response shape for GET /api/cloudwatch-traces */ +export interface ListCloudWatchTracesResponse { + success: boolean; + traces?: CloudWatchTraceEntry[]; + error?: string; +} + +// --------------------------------------------------------------------------- +// GET /api/cloudwatch-traces/:traceId?agentName=xxx|harnessName=xxx +// --------------------------------------------------------------------------- + +/** Response shape for GET /api/cloudwatch-traces/:traceId */ +export interface GetCloudWatchTraceResponse { + success: boolean; + records?: CloudWatchTraceRecord[]; + spans?: CloudWatchSpanRecord[]; + error?: string; +} + +export type { CloudWatchTraceRecord, CloudWatchSpanRecord } from '../../traces/types'; + // --------------------------------------------------------------------------- // GET /api/memory?memoryName=xxx&namespace=yyy[&strategyId=zzz] // --------------------------------------------------------------------------- diff --git a/src/cli/operations/dev/web-ui/handlers/cloudwatch-traces.ts b/src/cli/operations/dev/web-ui/handlers/cloudwatch-traces.ts new file mode 100644 index 000000000..15759b766 --- /dev/null +++ b/src/cli/operations/dev/web-ui/handlers/cloudwatch-traces.ts @@ -0,0 +1,166 @@ +import type { RouteContext } from './route-context'; +import { parseRequestUrl } from './route-context'; +import type { IncomingMessage, ServerResponse } from 'node:http'; + +/** + * GET /api/cloudwatch-traces?agentName=xxx or ?harnessName=xxx — list recent CloudWatch traces. + * Exactly one of agentName or harnessName must be provided. + */ +export async function handleListCloudWatchTraces( + ctx: RouteContext, + req: IncomingMessage, + res: ServerResponse, + origin?: string +): Promise { + const { param } = parseRequestUrl(req); + const handler = ctx.options.onListCloudWatchTraces; + + if (!handler) { + ctx.setCorsHeaders(res, origin); + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'CloudWatch traces are not available' })); + return; + } + + const agentName = param('agentName'); + const harnessName = param('harnessName'); + + if (!agentName && !harnessName) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Either agentName or harnessName query parameter is required' })); + return; + } + + if (agentName && harnessName) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + success: false, + error: 'Provide either agentName or harnessName, not both', + }) + ); + return; + } + + // Parse optional date range query params (epoch milliseconds) + const startTimeRaw = param('startTime'); + const endTimeRaw = param('endTime'); + const startTime = startTimeRaw ? Number(startTimeRaw) : undefined; + const endTime = endTimeRaw ? Number(endTimeRaw) : undefined; + + if (startTimeRaw && isNaN(startTime!)) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'startTime must be a number (epoch milliseconds)' })); + return; + } + if (endTimeRaw && isNaN(endTime!)) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'endTime must be a number (epoch milliseconds)' })); + return; + } + + try { + const result = await handler(agentName, harnessName, startTime, endTime); + ctx.setCorsHeaders(res, origin); + res.writeHead(result.success ? 200 : 500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(result)); + } catch (err) { + ctx.options.onLog?.('error', `List CloudWatch traces error: ${err instanceof Error ? err.message : String(err)}`); + ctx.setCorsHeaders(res, origin); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Failed to list CloudWatch traces' })); + } +} + +/** + * GET /api/cloudwatch-traces/:traceId?agentName=xxx or ?harnessName=xxx — get full CloudWatch trace data. + * Exactly one of agentName or harnessName must be provided. + */ +export async function handleGetCloudWatchTrace( + ctx: RouteContext, + req: IncomingMessage, + res: ServerResponse, + origin?: string +): Promise { + const { pathname, param } = parseRequestUrl(req); + const handler = ctx.options.onGetCloudWatchTrace; + + if (!handler) { + ctx.setCorsHeaders(res, origin); + res.writeHead(404, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'CloudWatch traces are not available' })); + return; + } + + const traceId = pathname.replace('/api/cloudwatch-traces/', ''); + const agentName = param('agentName'); + const harnessName = param('harnessName'); + + if (!traceId) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'traceId is required in the URL path' })); + return; + } + + if (!/^[a-fA-F0-9-]+$/.test(traceId)) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Invalid trace ID format' })); + return; + } + + if (!agentName && !harnessName) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Either agentName or harnessName query parameter is required' })); + return; + } + + if (agentName && harnessName) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end( + JSON.stringify({ + success: false, + error: 'Provide either agentName or harnessName, not both', + }) + ); + return; + } + + // Parse optional date range query params (epoch milliseconds) + const startTimeRaw = param('startTime'); + const endTimeRaw = param('endTime'); + const startTime = startTimeRaw ? Number(startTimeRaw) : undefined; + const endTime = endTimeRaw ? Number(endTimeRaw) : undefined; + + if (startTimeRaw && isNaN(startTime!)) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'startTime must be a number (epoch milliseconds)' })); + return; + } + if (endTimeRaw && isNaN(endTime!)) { + ctx.setCorsHeaders(res, origin); + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'endTime must be a number (epoch milliseconds)' })); + return; + } + + try { + const result = await handler(agentName, harnessName, traceId, startTime, endTime); + ctx.setCorsHeaders(res, origin); + res.writeHead(result.success ? 200 : 500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify(result)); + } catch (err) { + ctx.options.onLog?.('error', `Get CloudWatch trace error: ${err instanceof Error ? err.message : String(err)}`); + ctx.setCorsHeaders(res, origin); + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ success: false, error: 'Failed to get CloudWatch trace' })); + } +} diff --git a/src/cli/operations/dev/web-ui/handlers/index.ts b/src/cli/operations/dev/web-ui/handlers/index.ts index 91d2d4d5d..0ae7b4f67 100644 --- a/src/cli/operations/dev/web-ui/handlers/index.ts +++ b/src/cli/operations/dev/web-ui/handlers/index.ts @@ -4,6 +4,7 @@ export { handleResources } from './resources'; export { handleStart } from './start'; export { handleInvocations } from './invocations'; export { handleListTraces, handleGetTrace } from './traces'; +export { handleListCloudWatchTraces, handleGetCloudWatchTrace } from './cloudwatch-traces'; export { handleListMemoryRecords, handleRetrieveMemoryRecords } from './memory'; export { handleMcpProxy } from './mcp-proxy'; export { handleA2AAgentCard } from './a2a-proxy'; diff --git a/src/cli/operations/dev/web-ui/index.ts b/src/cli/operations/dev/web-ui/index.ts index 6901eb31a..b14949008 100644 --- a/src/cli/operations/dev/web-ui/index.ts +++ b/src/cli/operations/dev/web-ui/index.ts @@ -4,6 +4,8 @@ export { type StartHandler, type ListTracesHandler, type GetTraceHandler, + type ListCloudWatchTracesHandler, + type GetCloudWatchTraceHandler, type ListMemoryRecordsHandler, type RetrieveMemoryRecordsHandler, } from './web-server'; @@ -29,6 +31,11 @@ export type { InvocationRequest, ListTracesResponse, GetTraceResponse, + ListCloudWatchTracesResponse, + CloudWatchTraceEntry, + GetCloudWatchTraceResponse, + CloudWatchTraceRecord, + CloudWatchSpanRecord, ListMemoryRecordsResponse, MemoryRecordResponse, RetrieveMemoryRecordsRequest, diff --git a/src/cli/operations/dev/web-ui/web-server.ts b/src/cli/operations/dev/web-ui/web-server.ts index c3f9c6f36..2b20b2d07 100644 --- a/src/cli/operations/dev/web-ui/web-server.ts +++ b/src/cli/operations/dev/web-ui/web-server.ts @@ -4,8 +4,10 @@ import { type AgentError, type AgentInfo, WEB_UI_LOCAL_URL } from './constants'; import { type RouteContext, handleA2AAgentCard, + handleGetCloudWatchTrace, handleGetTrace, handleInvocations, + handleListCloudWatchTraces, handleListMemoryRecords, handleListTraces, handleMcpProxy, @@ -78,6 +80,29 @@ export type GetTraceHandler = ( endTime?: number ) => Promise<{ success: boolean; resourceSpans?: unknown[]; resourceLogs?: unknown[]; error?: string }>; +/** + * Custom handler for GET /api/cloudwatch-traces. + * Returns a list of recent CloudWatch traces for the given agent or harness. + */ +export type ListCloudWatchTracesHandler = ( + agentName: string | undefined, + harnessName: string | undefined, + startTime?: number, + endTime?: number +) => Promise<{ success: boolean; traces?: unknown[]; error?: string }>; + +/** + * Custom handler for GET /api/cloudwatch-traces/:traceId. + * Returns the full CloudWatch trace data for a specific trace. + */ +export type GetCloudWatchTraceHandler = ( + agentName: string | undefined, + harnessName: string | undefined, + traceId: string, + startTime?: number, + endTime?: number +) => Promise<{ success: boolean; records?: unknown[]; spans?: unknown[]; error?: string }>; + /** * Custom handler for GET /api/memory. * Returns a list of memory records for a given memory + namespace. @@ -124,6 +149,10 @@ export interface WebUIOptions { onListTraces?: ListTracesHandler; /** Custom handler for getting a single trace */ onGetTrace?: GetTraceHandler; + /** Custom handler for listing CloudWatch traces */ + onListCloudWatchTraces?: ListCloudWatchTracesHandler; + /** Custom handler for getting a single CloudWatch trace */ + onGetCloudWatchTrace?: GetCloudWatchTraceHandler; /** Custom handler for listing memory records */ onListMemoryRecords?: ListMemoryRecordsHandler; /** Custom handler for searching memory records */ @@ -291,6 +320,10 @@ export class WebUIServer { await handleGetTrace(ctx, req, res, origin); } else if (req.method === 'GET' && req.url?.startsWith('/api/traces')) { await handleListTraces(ctx, req, res, origin); + } else if (req.method === 'GET' && req.url?.startsWith('/api/cloudwatch-traces/')) { + await handleGetCloudWatchTrace(ctx, req, res, origin); + } else if (req.method === 'GET' && req.url?.startsWith('/api/cloudwatch-traces')) { + await handleListCloudWatchTraces(ctx, req, res, origin); } else if (req.method === 'POST' && req.url === '/api/start') { await handleStart(ctx, req, res, origin); } else if (req.method === 'POST' && req.url === '/invocations') { diff --git a/src/cli/operations/traces/__tests__/get-trace.test.ts b/src/cli/operations/traces/__tests__/get-trace.test.ts new file mode 100644 index 000000000..c6fda22f4 --- /dev/null +++ b/src/cli/operations/traces/__tests__/get-trace.test.ts @@ -0,0 +1,233 @@ +import { fetchTraceRecords, getTrace } from '../get-trace'; +import type { FetchTraceRecordsOptions } from '../types'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const { mockSend } = vi.hoisted(() => ({ + mockSend: vi.fn(), +})); + +vi.mock('@aws-sdk/client-cloudwatch-logs', () => ({ + CloudWatchLogsClient: class { + send = mockSend; + }, + StartQueryCommand: class { + constructor(public input: unknown) {} + }, + GetQueryResultsCommand: class { + constructor(public input: unknown) {} + }, +})); + +vi.mock('../../../aws', () => ({ + getCredentialProvider: vi.fn().mockReturnValue({}), +})); + +vi.mock('node:fs', () => ({ + default: { + mkdirSync: vi.fn(), + writeFileSync: vi.fn(), + }, +})); + +const baseOptions: FetchTraceRecordsOptions = { + region: 'us-west-2', + runtimeId: 'runtime-123', + traceId: 'abc123def456', + startTime: 1000000, + endTime: 2000000, +}; + +describe('fetchTraceRecords', () => { + afterEach(() => vi.clearAllMocks()); + + it('returns parsed trace records from CloudWatch', async () => { + mockSend + .mockResolvedValueOnce({ queryId: 'q-1' }) // StartQueryCommand + .mockResolvedValueOnce({ + // GetQueryResultsCommand + status: 'Complete', + results: [ + [ + { field: '@timestamp', value: '2024-01-01T00:00:00Z' }, + { field: '@message', value: '{"traceId":"abc123","spanId":"span1"}' }, + { field: '@ptr', value: 'ptr-value-1' }, + ], + [ + { field: '@timestamp', value: '2024-01-01T00:00:01Z' }, + { field: '@message', value: '{"traceId":"abc123","spanId":"span2"}' }, + ], + ], + }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(true); + expect(result.records).toHaveLength(2); + expect(result.records![0]).toEqual({ + '@timestamp': '2024-01-01T00:00:00Z', + '@message': { traceId: 'abc123', spanId: 'span1' }, + '@ptr': 'ptr-value-1', + }); + expect(result.records![1]).toEqual({ + '@timestamp': '2024-01-01T00:00:01Z', + '@message': { traceId: 'abc123', spanId: 'span2' }, + }); + }); + + it('returns error for invalid trace ID format', async () => { + const result = await fetchTraceRecords({ + ...baseOptions, + traceId: 'invalid!@#$', + }); + + expect(result.success).toBe(false); + expect(result.error).toContain('Invalid trace ID format'); + expect(mockSend).not.toHaveBeenCalled(); + }); + + it('returns error when no trace data found', async () => { + mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({ + status: 'Complete', + results: [], + }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(false); + expect(result.error).toContain('No trace data found'); + }); + + it('returns error when query fails to start', async () => { + mockSend.mockResolvedValueOnce({ queryId: undefined }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(false); + expect(result.error).toContain('Failed to start CloudWatch Logs Insights query'); + }); + + it('returns error when query status is Failed', async () => { + mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({ status: 'Failed' }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(false); + expect(result.error).toContain('failed'); + }); + + it('preserves @ptr when present in CloudWatch response', async () => { + mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({ + status: 'Complete', + results: [ + [ + { field: '@timestamp', value: '2024-01-01T00:00:00Z' }, + { field: '@message', value: '{"key":"val"}' }, + { field: '@ptr', value: 'cw-ptr-123' }, + ], + ], + }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(true); + expect(result.records).toHaveLength(1); + expect(result.records![0]!['@ptr']).toBe('cw-ptr-123'); + }); + + it('omits @ptr when not present in CloudWatch response', async () => { + mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({ + status: 'Complete', + results: [ + [ + { field: '@timestamp', value: '2024-01-01T00:00:00Z' }, + { field: '@message', value: '{"key":"val"}' }, + ], + ], + }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(true); + expect(result.records![0]).not.toHaveProperty('@ptr'); + }); + + it('handles non-JSON @message gracefully', async () => { + mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({ + status: 'Complete', + results: [ + [ + { field: '@timestamp', value: '2024-01-01T00:00:00Z' }, + { field: '@message', value: 'plain text message' }, + ], + ], + }); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(true); + expect(result.records).toHaveLength(1); + expect(result.records![0]!['@message']).toBe('plain text message'); + }); + + it('handles ResourceNotFoundException', async () => { + const error = new Error('Not found'); + error.name = 'ResourceNotFoundException'; + mockSend.mockRejectedValueOnce(error); + + const result = await fetchTraceRecords(baseOptions); + + expect(result.success).toBe(false); + expect(result.error).toContain('Log group'); + expect(result.error).toContain('not found'); + }); +}); + +describe('getTrace', () => { + afterEach(() => vi.clearAllMocks()); + + it('calls fetchTraceRecords and writes result to disk', async () => { + const fs = await import('node:fs'); + + mockSend.mockResolvedValueOnce({ queryId: 'q-1' }).mockResolvedValueOnce({ + status: 'Complete', + results: [ + [ + { field: '@timestamp', value: '2024-01-01T00:00:00Z' }, + { field: '@message', value: '{"traceId":"abc123"}' }, + ], + ], + }); + + const result = await getTrace({ + region: 'us-west-2', + runtimeId: 'runtime-123', + agentName: 'my-agent', + traceId: 'abc123def456', + outputPath: '/tmp/test-trace.json', + startTime: 1000000, + endTime: 2000000, + }); + + expect(result.success).toBe(true); + expect(result.filePath).toContain('test-trace.json'); + expect(fs.default.mkdirSync).toHaveBeenCalled(); + expect(fs.default.writeFileSync).toHaveBeenCalledWith('/tmp/test-trace.json', expect.stringContaining('"traceId"')); + }); + + it('returns error from fetchTraceRecords without writing file', async () => { + const fs = await import('node:fs'); + + const result = await getTrace({ + region: 'us-west-2', + runtimeId: 'runtime-123', + agentName: 'my-agent', + traceId: 'invalid!@#$', + startTime: 1000000, + endTime: 2000000, + }); + + expect(result.success).toBe(false); + expect(result.error).toContain('Invalid trace ID format'); + expect(fs.default.writeFileSync).not.toHaveBeenCalled(); + }); +}); diff --git a/src/cli/operations/traces/__tests__/list-traces.test.ts b/src/cli/operations/traces/__tests__/list-traces.test.ts new file mode 100644 index 000000000..0bbe884de --- /dev/null +++ b/src/cli/operations/traces/__tests__/list-traces.test.ts @@ -0,0 +1,135 @@ +import { listTraces } from '../list-traces'; +import type { ListTracesOptions } from '../types'; +import { afterEach, describe, expect, it, vi } from 'vitest'; + +const { mockRunInsightsQuery } = vi.hoisted(() => ({ + mockRunInsightsQuery: vi.fn(), +})); + +vi.mock('../insights-query', () => ({ + runInsightsQuery: mockRunInsightsQuery, +})); + +const baseOptions: ListTracesOptions = { + region: 'us-west-2', + runtimeId: 'runtime-123', + agentName: 'my-agent', + startTime: 1000000, + endTime: 2000000, +}; + +describe('listTraces', () => { + afterEach(() => vi.clearAllMocks()); + + it('returns trace entries from query results', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ + success: true, + rows: [ + { + traceId: 'trace-1', + lastSeen: '2024-01-01T00:05:00Z', + firstSeen: '2024-01-01T00:00:00Z', + spanCount: '12', + sessionId: 'sess-1', + }, + { traceId: 'trace-2', lastSeen: '2024-01-01T00:03:00Z', firstSeen: '2024-01-01T00:01:00Z', spanCount: '5' }, + ], + }); + + const result = await listTraces(baseOptions); + + expect(result.success).toBe(true); + expect(result.traces).toHaveLength(2); + expect(result.traces![0]).toEqual({ + traceId: 'trace-1', + timestamp: '2024-01-01T00:05:00Z', + sessionId: 'sess-1', + spanCount: '12', + }); + expect(result.traces![1]).toEqual({ + traceId: 'trace-2', + timestamp: '2024-01-01T00:03:00Z', + sessionId: undefined, + spanCount: '5', + }); + }); + + it('filters out rows without traceId', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ + success: true, + rows: [ + { traceId: 'trace-1', lastSeen: '2024-01-01T00:00:00Z', spanCount: '3' }, + { lastSeen: '2024-01-01T00:00:00Z', spanCount: '1' }, + { traceId: '', lastSeen: '2024-01-01T00:00:00Z', spanCount: '2' }, + ], + }); + + const result = await listTraces(baseOptions); + + expect(result.success).toBe(true); + expect(result.traces).toHaveLength(1); + expect(result.traces![0]!.traceId).toBe('trace-1'); + }); + + it('falls back to firstSeen when lastSeen is missing', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ + success: true, + rows: [{ traceId: 'trace-1', firstSeen: '2024-01-01T00:00:00Z', spanCount: '1' }], + }); + + const result = await listTraces(baseOptions); + + expect(result.success).toBe(true); + expect(result.traces![0]!.timestamp).toBe('2024-01-01T00:00:00Z'); + }); + + it('returns empty traces for empty query results', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ + success: true, + rows: [], + }); + + const result = await listTraces(baseOptions); + + expect(result.success).toBe(true); + expect(result.traces).toHaveLength(0); + }); + + it('propagates errors from runInsightsQuery', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ + success: false, + error: 'Log group not found', + }); + + const result = await listTraces(baseOptions); + + expect(result.success).toBe(false); + expect(result.error).toBe('Log group not found'); + }); + + it('passes correct log group name and default limit', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ success: true, rows: [] }); + + await listTraces(baseOptions); + + expect(mockRunInsightsQuery).toHaveBeenCalledWith({ + region: 'us-west-2', + logGroupName: '/aws/bedrock-agentcore/runtimes/runtime-123-DEFAULT', + startTime: 1000000, + endTime: 2000000, + queryString: expect.stringContaining('limit 20'), + }); + }); + + it('respects custom limit', async () => { + mockRunInsightsQuery.mockResolvedValueOnce({ success: true, rows: [] }); + + await listTraces({ ...baseOptions, limit: 50 }); + + expect(mockRunInsightsQuery).toHaveBeenCalledWith( + expect.objectContaining({ + queryString: expect.stringContaining('limit 50'), + }) + ); + }); +}); diff --git a/src/cli/operations/traces/get-trace.ts b/src/cli/operations/traces/get-trace.ts index 85c4471be..a87f10a65 100644 --- a/src/cli/operations/traces/get-trace.ts +++ b/src/cli/operations/traces/get-trace.ts @@ -1,129 +1,186 @@ -import { getCredentialProvider } from '../../aws'; import { DEFAULT_ENDPOINT_NAME } from '../../constants'; -import { CloudWatchLogsClient, GetQueryResultsCommand, StartQueryCommand } from '@aws-sdk/client-cloudwatch-logs'; +import { runInsightsQuery } from './insights-query'; +import type { + CloudWatchSpanRecord, + CloudWatchTraceRecord, + FetchTraceRecordsOptions, + FetchTraceRecordsResult, + GetTraceOptions, + GetTraceResult, +} from './types'; import fs from 'node:fs'; import path from 'node:path'; -export interface GetTraceOptions { - region: string; - runtimeId: string; - agentName: string; - traceId: string; - outputPath?: string; - startTime?: number; - endTime?: number; -} +const SPANS_LOG_GROUP = 'aws/spans'; +const TRACE_ID_PATTERN = /^[a-fA-F0-9-]+$/; -export interface GetTraceResult { - success: boolean; - filePath?: string; - error?: string; +function runtimeLogGroup(runtimeId: string): string { + return `/aws/bedrock-agentcore/runtimes/${runtimeId}-${DEFAULT_ENDPOINT_NAME}`; } -/** - * Fetches a full trace from CloudWatch Logs and writes it to a JSON file. - * - * Log group naming convention: /aws/bedrock-agentcore/runtimes/{runtimeId}-DEFAULT - * Trace ID is stored in the @message JSON body as "traceId". - */ -export async function getTrace(options: GetTraceOptions): Promise { - const { region, runtimeId, agentName, traceId, outputPath } = options; - - if (!/^[a-fA-F0-9-]+$/.test(traceId)) { +async function fetchSpans( + region: string, + traceId: string, + startTime?: number, + endTime?: number +): Promise<{ success: boolean; spans?: CloudWatchSpanRecord[]; error?: string }> { + if (!TRACE_ID_PATTERN.test(traceId)) { return { success: false, error: 'Invalid trace ID format. Expected a hex string (e.g., abc123def456).' }; } - const client = new CloudWatchLogsClient({ - credentials: getCredentialProvider(), + const result = await runInsightsQuery({ region, + logGroupName: SPANS_LOG_GROUP, + startTime, + endTime, + queryString: `fields traceId, spanId, parentSpanId, name, kind, + startTimeUnixNano, endTimeUnixNano, durationNano, + status.code as statusCode, + resource.attributes.service.name as serviceName, + attributes.gen_ai.usage.input_tokens as inputTokens, + attributes.gen_ai.usage.output_tokens as outputTokens, + attributes.gen_ai.usage.total_tokens as totalTokens, + attributes.http.status_code as httpStatusCode, + attributes.session.id as sessionId +| filter ispresent(traceId) and ispresent(resource.attributes.service.name) +| filter resource.attributes.aws.service.type = "gen_ai_agent" +| filter traceId = '${traceId}' +| sort startTimeUnixNano asc`, }); - const logGroupName = `/aws/bedrock-agentcore/runtimes/${runtimeId}-${DEFAULT_ENDPOINT_NAME}`; + if (!result.success) return { success: false, error: result.error }; + + const spans: CloudWatchSpanRecord[] = (result.rows ?? []) + .filter(row => row.traceId && row.spanId) + .map(row => ({ + traceId: row.traceId!, + spanId: row.spanId!, + parentSpanId: row.parentSpanId ?? undefined, + name: row.name ?? undefined, + kind: row.kind ?? undefined, + startTimeUnixNano: row.startTimeUnixNano ?? undefined, + endTimeUnixNano: row.endTimeUnixNano ?? undefined, + durationNano: row.durationNano ?? undefined, + statusCode: row.statusCode ?? undefined, + serviceName: row.serviceName ?? undefined, + inputTokens: row.inputTokens ? Number(row.inputTokens) : undefined, + outputTokens: row.outputTokens ? Number(row.outputTokens) : undefined, + totalTokens: row.totalTokens ? Number(row.totalTokens) : undefined, + httpStatusCode: row.httpStatusCode ? Number(row.httpStatusCode) : undefined, + sessionId: row.sessionId ?? undefined, + })); + + return { success: true, spans }; +} + +/** + * Fetches trace records from CloudWatch Logs Insights for a given trace ID. + * Returns typed records for the web UI API. Use `getTrace()` to write raw + * results to a JSON file on disk. + */ +export async function fetchTraceRecords(options: FetchTraceRecordsOptions): Promise { + const { region, runtimeId, traceId, includeSpans } = options; - const now = Date.now(); - const endTime = options.endTime ?? now; - const startTime = options.startTime ?? endTime - 12 * 60 * 60 * 1000; // default: last 12 hours + if (!TRACE_ID_PATTERN.test(traceId)) { + return { success: false, error: 'Invalid trace ID format. Expected a hex string (e.g., abc123def456).' }; + } - try { - const startQuery = await client.send( - new StartQueryCommand({ - logGroupName, - startTime: Math.floor(startTime / 1000), - endTime: Math.floor(endTime / 1000), - queryString: `fields @timestamp, @message + const [recordsResult, spansResult] = await Promise.all([ + runInsightsQuery({ + region, + logGroupName: runtimeLogGroup(runtimeId), + startTime: options.startTime, + endTime: options.endTime, + queryString: `fields @timestamp, @message, @ptr | filter traceId = '${traceId}' | sort @timestamp asc -| limit 1000`, - }) - ); +| limit 10000`, + }), + includeSpans ? fetchSpans(region, traceId, options.startTime, options.endTime) : Promise.resolve(undefined), + ]); - if (!startQuery.queryId) { - return { success: false, error: 'Failed to start CloudWatch Logs Insights query' }; - } + if (!recordsResult.success) { + return { success: false, error: recordsResult.error }; + } - // Poll for results - let traceData: Record[] = []; - let queryStatus = 'Running'; - - for (let i = 0; i < 60; i++) { - await new Promise(resolve => setTimeout(resolve, 1000)); - - const queryResults = await client.send(new GetQueryResultsCommand({ queryId: startQuery.queryId })); - - queryStatus = queryResults.status ?? 'Unknown'; - - if (queryStatus === 'Complete' || queryStatus === 'Failed' || queryStatus === 'Cancelled') { - if (queryStatus !== 'Complete') { - return { success: false, error: `Query ${queryStatus.toLowerCase()}` }; - } - - traceData = (queryResults.results ?? []).map(row => { - const fields: Record = {}; - for (const field of row) { - if (field.field && field.value) { - fields[field.field] = field.value; - } - } - return fields; - }); - break; - } - } + const traceData = recordsResult.rows ?? []; - if (queryStatus === 'Running') { - return { success: false, error: 'Query timed out after 60 seconds' }; - } + if (traceData.length === 0 && (!spansResult || (spansResult.spans ?? []).length === 0)) { + return { success: false, error: `No trace data found for trace ID: ${traceId}` }; + } - if (traceData.length === 0) { - return { success: false, error: `No trace data found for trace ID: ${traceId}` }; + const records: CloudWatchTraceRecord[] = traceData.map(entry => { + let message: unknown = entry['@message'] ?? '{}'; + try { + message = JSON.parse(entry['@message'] ?? '{}'); + } catch { + // Keep original string if not valid JSON } - // Parse @message fields as JSON where possible - const parsedTrace = traceData.map(entry => { - try { - const parsed: unknown = JSON.parse(entry['@message'] ?? '{}'); - return { ...entry, '@message': parsed }; - } catch { - return entry; - } - }); - - // Write to file - const filePath = outputPath ?? path.join('agentcore', '.cli', 'traces', `${agentName}-${traceId}.json`); - - const dir = path.dirname(filePath); - fs.mkdirSync(dir, { recursive: true }); - fs.writeFileSync(filePath, JSON.stringify(parsedTrace, null, 2)); - - return { success: true, filePath: path.resolve(filePath) }; - } catch (error: unknown) { - const err = error as Error; - if (err.name === 'ResourceNotFoundException') { - return { - success: false, - error: `Log group '${logGroupName}' not found. The agent may not have been invoked yet, or traces may not be enabled.`, - }; + const record: CloudWatchTraceRecord = { + '@timestamp': entry['@timestamp'] ?? '', + '@message': message, + }; + + if (entry['@ptr']) { + record['@ptr'] = entry['@ptr']; } - return { success: false, error: err.message ?? String(error) }; + + return record; + }); + + const result: FetchTraceRecordsResult = { success: true, records }; + + if (spansResult?.success && spansResult.spans) { + result.spans = spansResult.spans; } + + return result; +} + +/** + * Fetches a full trace from CloudWatch Logs and writes it to a JSON file. + * Preserves all raw CloudWatch Insights fields in the output file. + */ +export async function getTrace(options: GetTraceOptions): Promise { + const { region, runtimeId, agentName, traceId, outputPath } = options; + + if (!TRACE_ID_PATTERN.test(traceId)) { + return { success: false, error: 'Invalid trace ID format. Expected a hex string (e.g., abc123def456).' }; + } + + const result = await runInsightsQuery({ + region, + logGroupName: runtimeLogGroup(runtimeId), + startTime: options.startTime, + endTime: options.endTime, + queryString: `fields @timestamp, @message +| filter traceId = '${traceId}' +| sort @timestamp asc +| limit 10000`, + }); + if (!result.success) { + return { success: false, error: result.error }; + } + + const traceData = result.rows ?? []; + if (traceData.length === 0) { + return { success: false, error: `No trace data found for trace ID: ${traceId}` }; + } + + const parsedTrace = traceData.map(entry => { + try { + const parsed: unknown = JSON.parse(entry['@message'] ?? '{}'); + return { ...entry, '@message': parsed }; + } catch { + return entry; + } + }); + + const filePath = outputPath ?? path.join('agentcore', '.cli', 'traces', `${agentName}-${traceId}.json`); + const dir = path.dirname(filePath); + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(parsedTrace, null, 2)); + + return { success: true, filePath: path.resolve(filePath) }; } diff --git a/src/cli/operations/traces/index.ts b/src/cli/operations/traces/index.ts index bbb013439..cf19dbf9c 100644 --- a/src/cli/operations/traces/index.ts +++ b/src/cli/operations/traces/index.ts @@ -1,3 +1,15 @@ export { buildTraceConsoleUrl } from './trace-url'; -export { listTraces, type TraceEntry, type ListTracesOptions, type ListTracesResult } from './list-traces'; -export { getTrace, type GetTraceOptions, type GetTraceResult } from './get-trace'; +export { listTraces } from './list-traces'; +export { fetchTraceRecords, getTrace } from './get-trace'; +export { runInsightsQuery, type InsightsQueryOptions, type InsightsQueryResult } from './insights-query'; +export type { + CloudWatchSpanRecord, + CloudWatchTraceRecord, + FetchTraceRecordsOptions, + FetchTraceRecordsResult, + GetTraceOptions, + GetTraceResult, + ListTracesOptions, + ListTracesResult, + TraceEntry, +} from './types'; diff --git a/src/cli/operations/traces/insights-query.ts b/src/cli/operations/traces/insights-query.ts new file mode 100644 index 000000000..5a4da2031 --- /dev/null +++ b/src/cli/operations/traces/insights-query.ts @@ -0,0 +1,85 @@ +import { getCredentialProvider } from '../../aws'; +import { CloudWatchLogsClient, GetQueryResultsCommand, StartQueryCommand } from '@aws-sdk/client-cloudwatch-logs'; + +const DEFAULT_LOOKBACK_MS = 12 * 60 * 60 * 1000; + +export interface InsightsQueryOptions { + region: string; + logGroupName: string; + queryString: string; + startTime?: number; + endTime?: number; +} + +export interface InsightsQueryResult { + success: boolean; + rows?: Record[]; + error?: string; +} + +async function pollQueryResults(client: CloudWatchLogsClient, queryId: string): Promise { + for (let i = 0; i < 60; i++) { + await new Promise(resolve => setTimeout(resolve, 1000)); + + const queryResults = await client.send(new GetQueryResultsCommand({ queryId })); + const status = queryResults.status ?? 'Unknown'; + + if (status === 'Complete' || status === 'Failed' || status === 'Cancelled') { + if (status !== 'Complete') { + return { success: false, error: `Query ${status.toLowerCase()}` }; + } + + const rows = (queryResults.results ?? []).map(row => { + const fields: Record = {}; + for (const field of row) { + if (field.field && field.value) { + fields[field.field] = field.value; + } + } + return fields; + }); + return { success: true, rows }; + } + } + + return { success: false, error: 'Query timed out after 60 seconds' }; +} + +export async function runInsightsQuery(options: InsightsQueryOptions): Promise { + const { region, logGroupName, queryString } = options; + + const client = new CloudWatchLogsClient({ + credentials: getCredentialProvider(), + region, + }); + + const now = Date.now(); + const endTime = options.endTime ?? now; + const startTime = options.startTime ?? endTime - DEFAULT_LOOKBACK_MS; + + try { + const startQuery = await client.send( + new StartQueryCommand({ + logGroupName, + startTime: Math.floor(startTime / 1000), + endTime: Math.floor(endTime / 1000), + queryString, + }) + ); + + if (!startQuery.queryId) { + return { success: false, error: 'Failed to start CloudWatch Logs Insights query' }; + } + + return await pollQueryResults(client, startQuery.queryId); + } catch (error: unknown) { + const err = error as Error; + if (err.name === 'ResourceNotFoundException') { + return { + success: false, + error: `Log group '${logGroupName}' not found. The agent may not have been invoked yet, or traces may not be enabled.`, + }; + } + return { success: false, error: err.message ?? String(error) }; + } +} diff --git a/src/cli/operations/traces/list-traces.ts b/src/cli/operations/traces/list-traces.ts index 7bff6194a..e2d998578 100644 --- a/src/cli/operations/traces/list-traces.ts +++ b/src/cli/operations/traces/list-traces.ts @@ -1,28 +1,6 @@ -import { getCredentialProvider } from '../../aws'; import { DEFAULT_ENDPOINT_NAME } from '../../constants'; -import { CloudWatchLogsClient, GetQueryResultsCommand, StartQueryCommand } from '@aws-sdk/client-cloudwatch-logs'; - -export interface TraceEntry { - traceId: string; - timestamp: string; - sessionId?: string; - spanCount?: string; -} - -export interface ListTracesOptions { - region: string; - runtimeId: string; - agentName: string; - limit?: number; - startTime?: number; - endTime?: number; -} - -export interface ListTracesResult { - success: boolean; - traces?: TraceEntry[]; - error?: string; -} +import { runInsightsQuery } from './insights-query'; +import type { ListTracesOptions, ListTracesResult, TraceEntry } from './types'; /** * Lists recent traces for a deployed agent by querying CloudWatch Logs Insights. @@ -33,80 +11,33 @@ export interface ListTracesResult { export async function listTraces(options: ListTracesOptions): Promise { const { region, runtimeId, limit = 20 } = options; - const client = new CloudWatchLogsClient({ - credentials: getCredentialProvider(), - region, - }); - const logGroupName = `/aws/bedrock-agentcore/runtimes/${runtimeId}-${DEFAULT_ENDPOINT_NAME}`; - const now = Date.now(); - const endTime = options.endTime ?? now; - const startTime = options.startTime ?? endTime - 12 * 60 * 60 * 1000; // default: last 12 hours - - try { - const startQuery = await client.send( - new StartQueryCommand({ - logGroupName, - startTime: Math.floor(startTime / 1000), - endTime: Math.floor(endTime / 1000), - queryString: `stats earliest(@timestamp) as firstSeen, latest(@timestamp) as lastSeen, count(*) as spanCount, earliest(attributes.session.id) as sessionId by traceId + const result = await runInsightsQuery({ + region, + logGroupName, + startTime: options.startTime, + endTime: options.endTime, + queryString: `stats earliest(@timestamp) as firstSeen, latest(@timestamp) as lastSeen, count(*) as spanCount, earliest(attributes.session.id) as sessionId by traceId | sort lastSeen desc | limit ${limit}`, - }) - ); - - if (!startQuery.queryId) { - return { success: false, error: 'Failed to start CloudWatch Logs Insights query' }; - } - - // Poll for results - let status = 'Running'; - let results: TraceEntry[] = []; - - for (let i = 0; i < 60; i++) { - await new Promise(resolve => setTimeout(resolve, 1000)); - - const queryResults = await client.send(new GetQueryResultsCommand({ queryId: startQuery.queryId })); - - status = queryResults.status ?? 'Unknown'; - - if (status === 'Complete' || status === 'Failed' || status === 'Cancelled') { - if (status !== 'Complete') { - return { success: false, error: `Query ${status.toLowerCase()}` }; - } + }); - results = (queryResults.results ?? []).map(row => { - const fields: Record = {}; - for (const field of row) { - if (field.field && field.value) { - fields[field.field] = field.value; - } - } - return { - traceId: fields.traceId ?? 'unknown', - timestamp: fields.lastSeen ?? fields.firstSeen ?? 'unknown', - sessionId: fields.sessionId, - spanCount: fields.spanCount, - }; - }); - break; - } - } + if (!result.success) { + return { success: false, error: result.error }; + } - if (status === 'Running') { - return { success: false, error: 'Query timed out after 60 seconds' }; + const traces = (result.rows ?? []).reduce((acc, row) => { + if (row.traceId) { + acc.push({ + traceId: row.traceId, + timestamp: row.lastSeen ?? row.firstSeen ?? 'unknown', + sessionId: row.sessionId, + spanCount: row.spanCount, + }); } + return acc; + }, []); - return { success: true, traces: results }; - } catch (error: unknown) { - const err = error as Error; - if (err.name === 'ResourceNotFoundException') { - return { - success: false, - error: `Log group '${logGroupName}' not found. The agent may not have been invoked yet, or traces may not be enabled.`, - }; - } - return { success: false, error: err.message ?? String(error) }; - } + return { success: true, traces }; } diff --git a/src/cli/operations/traces/types.ts b/src/cli/operations/traces/types.ts new file mode 100644 index 000000000..fae88a83b --- /dev/null +++ b/src/cli/operations/traces/types.ts @@ -0,0 +1,77 @@ +export interface CloudWatchTraceRecord { + '@timestamp': string; + '@message': unknown; + '@ptr'?: string; +} + +export interface CloudWatchSpanRecord { + traceId: string; + spanId: string; + parentSpanId?: string; + name?: string; + kind?: string; + startTimeUnixNano?: string; + endTimeUnixNano?: string; + durationNano?: string; + statusCode?: string; + serviceName?: string; + inputTokens?: number; + outputTokens?: number; + totalTokens?: number; + httpStatusCode?: number; + sessionId?: string; +} + +export interface FetchTraceRecordsOptions { + region: string; + runtimeId: string; + traceId: string; + startTime?: number; + endTime?: number; + includeSpans?: boolean; +} + +export interface FetchTraceRecordsResult { + success: boolean; + records?: CloudWatchTraceRecord[]; + spans?: CloudWatchSpanRecord[]; + error?: string; +} + +export interface GetTraceOptions { + region: string; + runtimeId: string; + agentName: string; + traceId: string; + outputPath?: string; + startTime?: number; + endTime?: number; +} + +export interface GetTraceResult { + success: boolean; + filePath?: string; + error?: string; +} + +export interface TraceEntry { + traceId: string; + timestamp: string; + sessionId?: string; + spanCount?: string; +} + +export interface ListTracesOptions { + region: string; + runtimeId: string; + agentName: string; + limit?: number; + startTime?: number; + endTime?: number; +} + +export interface ListTracesResult { + success: boolean; + traces?: TraceEntry[]; + error?: string; +} From d1e52411e9b70caba7d5c6a22da7795c428341d8 Mon Sep 17 00:00:00 2001 From: Aidan Daly <99039782+aidandaly24@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:41:32 -0400 Subject: [PATCH 03/45] fix: remove CONFIG_DIR exclusion from zip stage to preserve dependency agentcore/ packages (#1015) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #844 correctly removed the flat name-based agentcore exclusion and threaded rootDir through copySourceTree, but the same CONFIG_DIR check remained in collectFiles/collectFilesSync (the zip stage). Since the zip stage operates on the staging directory — not the project root — the check incorrectly stripped any top-level agentcore/ Python package installed by uv (e.g., langgraph_checkpoint_aws/agentcore/) from the deployment artifact, causing ModuleNotFoundError at runtime. The CONFIG_DIR exclusion is only needed in copySourceTree (which copies from the project root into staging). By the time we zip, the project config dir was already filtered out — the only agentcore/ in staging is a legitimate dependency package. Closes #843 --- src/lib/packaging/__tests__/helpers.test.ts | 71 ++++++++++++++++++--- src/lib/packaging/helpers.ts | 14 ++-- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/src/lib/packaging/__tests__/helpers.test.ts b/src/lib/packaging/__tests__/helpers.test.ts index a554edb0e..f27c171fa 100644 --- a/src/lib/packaging/__tests__/helpers.test.ts +++ b/src/lib/packaging/__tests__/helpers.test.ts @@ -477,8 +477,11 @@ describe('nested agentcore directory is preserved (issue #843)', () => { }); // ── createZipFromDir (async) ── + // The zip stage should NOT exclude agentcore/ — that's copySourceTree's job. + // When zipping a staging directory, any agentcore/ present is a legitimate + // Python package installed by uv, not the project config dir. - it('zip: excludes top-level agentcore/ but includes nested agentcore/', async () => { + it('zip: does not exclude agentcore/ directories (staging has no project config)', async () => { const src = buildFixture(join(root, 'zip-async')); const zipPath = join(root, 'zip-async.zip'); @@ -487,21 +490,17 @@ describe('nested agentcore directory is preserved (issue #843)', () => { const zipBuffer = await readFile(zipPath); const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); - // Top-level agentcore/ should NOT appear - expect(entries.some(e => e === 'agentcore/config.yaml')).toBe(false); - expect(entries.some(e => e.startsWith('agentcore/'))).toBe(false); - - // Nested agentcore/ SHOULD appear + // Both top-level and nested agentcore/ are preserved in the zip — + // the zip function zips everything; exclusion is copySourceTree's concern + expect(entries).toContain('agentcore/config.yaml'); expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/__init__.py'); expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/core.py'); - - // Regular files present expect(entries).toContain('main.py'); }); // ── createZipFromDirSync ── - it('sync zip: excludes top-level agentcore/ but includes nested agentcore/', () => { + it('sync zip: does not exclude agentcore/ directories (staging has no project config)', () => { const src = buildFixture(join(root, 'zip-sync')); const zipPath = join(root, 'zip-sync.zip'); @@ -510,9 +509,61 @@ describe('nested agentcore directory is preserved (issue #843)', () => { const zipBuffer = readFileSync(zipPath); const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); - expect(entries.some(e => e.startsWith('agentcore/'))).toBe(false); + expect(entries).toContain('agentcore/config.yaml'); expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/__init__.py'); expect(entries).toContain('lib/langgraph_checkpoint_aws/agentcore/core.py'); expect(entries).toContain('main.py'); }); + + // ── Staging directory scenario (the actual bug) ── + // After uv installs deps into staging, copySourceTree copies user source on top. + // The staging dir may contain a top-level agentcore/ from a Python package. + // createZipFromDir must NOT strip it. + + it('zip preserves top-level agentcore/ Python package in staging dir', async () => { + const staging = join(root, 'staging-zip-async'); + mkdirSync(staging, { recursive: true }); + + // Simulate uv-installed dependency with top-level agentcore/ package + const agentcorePkg = join(staging, 'langgraph_checkpoint_aws', 'agentcore'); + mkdirSync(agentcorePkg, { recursive: true }); + writeFileSync(join(staging, 'langgraph_checkpoint_aws', '__init__.py'), '# init'); + writeFileSync(join(agentcorePkg, '__init__.py'), '# agentcore init'); + writeFileSync(join(agentcorePkg, 'saver.py'), 'class AgentCoreMemorySaver: pass'); + + // User source copied on top by copySourceTree + writeFileSync(join(staging, 'main.py'), 'print("hello")'); + + const zipPath = join(root, 'staging-async.zip'); + await createZipFromDir(staging, zipPath); + + const zipBuffer = await readFile(zipPath); + const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); + + expect(entries).toContain('langgraph_checkpoint_aws/agentcore/__init__.py'); + expect(entries).toContain('langgraph_checkpoint_aws/agentcore/saver.py'); + expect(entries).toContain('main.py'); + }); + + it('sync zip preserves top-level agentcore/ Python package in staging dir', () => { + const staging = join(root, 'staging-zip-sync'); + mkdirSync(staging, { recursive: true }); + + const agentcorePkg = join(staging, 'langgraph_checkpoint_aws', 'agentcore'); + mkdirSync(agentcorePkg, { recursive: true }); + writeFileSync(join(staging, 'langgraph_checkpoint_aws', '__init__.py'), '# init'); + writeFileSync(join(agentcorePkg, '__init__.py'), '# agentcore init'); + writeFileSync(join(agentcorePkg, 'saver.py'), 'class AgentCoreMemorySaver: pass'); + writeFileSync(join(staging, 'main.py'), 'print("hello")'); + + const zipPath = join(root, 'staging-sync.zip'); + createZipFromDirSync(staging, zipPath); + + const zipBuffer = readFileSync(zipPath); + const entries = Object.keys(unzipSync(new Uint8Array(zipBuffer))); + + expect(entries).toContain('langgraph_checkpoint_aws/agentcore/__init__.py'); + expect(entries).toContain('langgraph_checkpoint_aws/agentcore/saver.py'); + expect(entries).toContain('main.py'); + }); }); diff --git a/src/lib/packaging/helpers.ts b/src/lib/packaging/helpers.ts index 31c74b298..36074395c 100644 --- a/src/lib/packaging/helpers.ts +++ b/src/lib/packaging/helpers.ts @@ -192,24 +192,23 @@ export async function createZipFromDir(sourceDir: string, outputZip: string): Pr await rm(outputZip, { force: true }); await mkdir(dirname(outputZip), { recursive: true }); - const files = await collectFiles(sourceDir, sourceDir); + const files = await collectFiles(sourceDir); const zipped = zipSync(files); await writeFile(outputZip, zipped); } -async function collectFiles(directory: string, rootDir: string, basePath = ''): Promise { +async function collectFiles(directory: string, basePath = ''): Promise { const result: Zippable = {}; const entries = await readdir(directory, { withFileTypes: true }); for (const entry of entries) { if (EXCLUDED_ENTRIES.has(entry.name)) continue; - if (entry.name === CONFIG_DIR && resolve(directory) === resolve(rootDir)) continue; const fullPath = join(directory, entry.name); const zipPath = basePath ? `${basePath}/${entry.name}` : entry.name; if (entry.isDirectory()) { - Object.assign(result, await collectFiles(fullPath, rootDir, zipPath)); + Object.assign(result, await collectFiles(fullPath, zipPath)); } else if (entry.isFile()) { result[zipPath] = [await readFile(fullPath), { level: 6 }]; } @@ -325,19 +324,18 @@ export function ensureBinaryAvailableSync(binary: string, installHint?: string): throw new MissingDependencyError(binary, installHint); } -function collectFilesSync(directory: string, rootDir: string, basePath = ''): Zippable { +function collectFilesSync(directory: string, basePath = ''): Zippable { const result: Zippable = {}; const entries = readdirSync(directory, { withFileTypes: true }); for (const entry of entries) { if (EXCLUDED_ENTRIES.has(entry.name)) continue; - if (entry.name === CONFIG_DIR && resolve(directory) === resolve(rootDir)) continue; const fullPath = join(directory, entry.name); const zipPath = basePath ? `${basePath}/${entry.name}` : entry.name; if (entry.isDirectory()) { - Object.assign(result, collectFilesSync(fullPath, rootDir, zipPath)); + Object.assign(result, collectFilesSync(fullPath, zipPath)); } else if (entry.isFile()) { result[zipPath] = [readFileSync(fullPath), { level: 6 }]; } @@ -349,7 +347,7 @@ export function createZipFromDirSync(sourceDir: string, outputZip: string): void rmSync(outputZip, { force: true }); mkdirSync(dirname(outputZip), { recursive: true }); - const files = collectFilesSync(sourceDir, sourceDir); + const files = collectFilesSync(sourceDir); const zipped = zipSync(files); writeFileSync(outputZip, zipped); } From 7e8cae4b2a9bfaf2346633747390fc07b22e17a0 Mon Sep 17 00:00:00 2001 From: Gitika <53349492+notgitika@users.noreply.github.com> Date: Wed, 29 Apr 2026 11:58:49 -0400 Subject: [PATCH 04/45] ci: add coordinated main + preview release workflow (#995) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: add coordinated main + preview release workflow Adds a single workflow_dispatch that releases both branches together, ensuring they stay in sync on npm. * fix: address review — bump script compat, pre-publish verification, drop unused artifacts - Preview bump now uses `prerelease --prerelease-tag preview` which the bump-version.ts script actually accepts - Added verify-merges job that checks both main and preview have the expected versions before either publish runs (prevents drift) - Both publish jobs now depend on verify-merges instead of each other, so neither publishes unless both PRs are confirmed merged - Removed upload-artifact steps from test jobs since publish jobs rebuild from source post-merge * fix: auto-rebase preview onto main in preflight step Instead of failing when preview isn't rebased, the workflow now rebases automatically. If there are conflicts, it aborts and directs the user to resolve manually. * ci: add sync-preview workflow, simplify release preflight - New sync-preview.yml: runs on every push to main, auto-rebases preview onto main. Silently skips on conflicts (no failure). - Release workflow preflight reverted to a simple check — relies on sync-preview having already done the rebase. * fix: use merge instead of rebase for preview sync Rebase overwrites preview-specific values (package version, tests). Merge preserves preview's divergent files and only conflicts when both branches touch the same lines. * fix: concurrency control, conflict notifications, CDK tag TODO - Add concurrency group to sync-preview to prevent parallel race - On merge conflict, auto-create a GitHub issue (deduplicated) instead of silently skipping - Add TODO comment for CDK preview dist-tag in prepare-preview * fix: create PR with conflict markers instead of issue on merge conflict On conflict, sync-preview now: - Creates a branch with the merge conflict markers committed - Opens a PR targeting preview with resolution instructions - Tags the original commit author for visibility - Deduplicates (skips if a sync PR is already open) --- .../workflows/release-main-and-preview.yml | 520 ++++++++++++++++++ .github/workflows/sync-preview.yml | 125 +++++ 2 files changed, 645 insertions(+) create mode 100644 .github/workflows/release-main-and-preview.yml create mode 100644 .github/workflows/sync-preview.yml diff --git a/.github/workflows/release-main-and-preview.yml b/.github/workflows/release-main-and-preview.yml new file mode 100644 index 000000000..6f40bf756 --- /dev/null +++ b/.github/workflows/release-main-and-preview.yml @@ -0,0 +1,520 @@ +name: Release Both (Main + Preview) + +on: + workflow_dispatch: + inputs: + main_bump_type: + description: 'Main branch version bump' + required: true + type: choice + options: + - patch + - minor + - major + preview_bump_type: + description: 'Preview branch version bump (prerelease with preview tag)' + required: true + type: choice + options: + - prerelease + main_changelog: + description: 'Main changelog entry (optional)' + required: false + type: string + preview_changelog: + description: 'Preview changelog entry (optional)' + required: false + type: string + dry_run: + description: 'Dry run — create PRs but skip npm publish' + required: false + type: boolean + default: false + +permissions: + contents: write + pull-requests: write + +jobs: + # ═══════════════════════════════════════════════════════════════════ + # Preflight — verify preview contains all of main + # ═══════════════════════════════════════════════════════════════════ + preflight: + name: Preflight Checks + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Verify running from main + run: | + if [[ "${{ github.ref }}" != "refs/heads/main" ]]; then + echo "❌ This workflow must be run from the main branch." + exit 1 + fi + + - name: Verify preview contains all of main + run: | + git fetch origin preview + MAIN_SHA=$(git rev-parse HEAD) + MERGE_BASE=$(git merge-base HEAD origin/preview) + + if [[ "$MAIN_SHA" != "$MERGE_BASE" ]]; then + echo "❌ preview branch does not contain all of main." + echo "" + echo "Main HEAD: $MAIN_SHA" + echo "Merge base: $MERGE_BASE" + echo "" + echo "The sync-preview workflow should have merged automatically." + echo "If it failed due to conflicts, resolve manually:" + echo " git checkout preview && git merge main && git push origin preview" + echo "" + echo "Then re-run this workflow." + exit 1 + fi + + echo "✅ preview contains all of main" + + # ═══════════════════════════════════════════════════════════════════ + # Step 1 — Prepare main release (bump, PR) + # ═══════════════════════════════════════════════════════════════════ + prepare-main: + name: Prepare Main Release + needs: preflight + runs-on: ubuntu-latest + outputs: + version: ${{ steps.bump.outputs.version }} + branch: ${{ steps.bump.outputs.branch }} + + steps: + - name: Checkout main + uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version: 20.x + + - name: Configure git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - run: npm ci + + - name: Sync @aws/agentcore-cdk to latest npm version + run: | + LATEST_CDK=$(npm view @aws/agentcore-cdk version 2>/dev/null || echo "") + if [ -n "$LATEST_CDK" ]; then + TEMPLATE_PKG="src/assets/cdk/package.json" + CURRENT_CDK=$(node -p "require('./$TEMPLATE_PKG').dependencies['@aws/agentcore-cdk']") + if [ "$CURRENT_CDK" != "$LATEST_CDK" ]; then + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('$TEMPLATE_PKG', 'utf8')); + pkg.dependencies['@aws/agentcore-cdk'] = '$LATEST_CDK'; + fs.writeFileSync('$TEMPLATE_PKG', JSON.stringify(pkg, null, 2) + '\n'); + " + echo "✅ Updated @aws/agentcore-cdk: $CURRENT_CDK -> $LATEST_CDK" + fi + fi + + - name: Bump version + id: bump + env: + BUMP_TYPE: ${{ github.event.inputs.main_bump_type }} + CHANGELOG_INPUT: ${{ github.event.inputs.main_changelog }} + run: | + BUMP_CMD="npx tsx scripts/bump-version.ts $BUMP_TYPE" + if [ -n "$CHANGELOG_INPUT" ]; then + BUMP_CMD="$BUMP_CMD --changelog \"$CHANGELOG_INPUT\"" + fi + eval $BUMP_CMD + + NEW_VERSION=$(node -p "require('./package.json').version") + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "branch=release/v$NEW_VERSION" >> $GITHUB_OUTPUT + echo "📦 Main version: $NEW_VERSION" + + - name: Regenerate JSON schema + run: | + npm run build + node scripts/generate-schema.mjs + npx prettier --write schemas/ + + - name: Create release branch and PR + env: + GH_TOKEN: ${{ github.token }} + NEW_VERSION: ${{ steps.bump.outputs.version }} + run: | + BRANCH_NAME="release/v$NEW_VERSION" + git ls-remote --exit-code --heads origin $BRANCH_NAME && git push origin --delete $BRANCH_NAME || true + git show-ref --verify --quiet refs/heads/$BRANCH_NAME && git branch -D $BRANCH_NAME || true + + git checkout -b $BRANCH_NAME + git add -A + git commit -m "chore: bump version to $NEW_VERSION" + git push origin $BRANCH_NAME + + gh pr create \ + --base main \ + --head "$BRANCH_NAME" \ + --title "Release v$NEW_VERSION" \ + --body "## Release v$NEW_VERSION (main) + + Part of a coordinated main + preview release. + + ### Checklist + - [ ] Review CHANGELOG.md + - [ ] All CI checks passing + - [ ] Merge this PR before approving the publish step" + + # ═══════════════════════════════════════════════════════════════════ + # Step 2 — Prepare preview release (bump, PR) + # ═══════════════════════════════════════════════════════════════════ + prepare-preview: + name: Prepare Preview Release + needs: preflight + runs-on: ubuntu-latest + outputs: + version: ${{ steps.bump.outputs.version }} + branch: ${{ steps.bump.outputs.branch }} + + steps: + - name: Checkout preview + uses: actions/checkout@v6 + with: + ref: preview + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version: 20.x + + - name: Configure git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - run: npm ci + + # TODO: When @aws/agentcore-cdk publishes a @preview dist-tag, change this + # to: npm view @aws/agentcore-cdk@preview version + - name: Sync @aws/agentcore-cdk to latest npm version + run: | + LATEST_CDK=$(npm view @aws/agentcore-cdk version 2>/dev/null || echo "") + if [ -n "$LATEST_CDK" ]; then + TEMPLATE_PKG="src/assets/cdk/package.json" + CURRENT_CDK=$(node -p "require('./$TEMPLATE_PKG').dependencies['@aws/agentcore-cdk']") + if [ "$CURRENT_CDK" != "$LATEST_CDK" ]; then + node -e " + const fs = require('fs'); + const pkg = JSON.parse(fs.readFileSync('$TEMPLATE_PKG', 'utf8')); + pkg.dependencies['@aws/agentcore-cdk'] = '$LATEST_CDK'; + fs.writeFileSync('$TEMPLATE_PKG', JSON.stringify(pkg, null, 2) + '\n'); + " + echo "✅ Updated @aws/agentcore-cdk: $CURRENT_CDK -> $LATEST_CDK" + fi + fi + + - name: Bump version + id: bump + env: + CHANGELOG_INPUT: ${{ github.event.inputs.preview_changelog }} + run: | + BUMP_CMD="npx tsx scripts/bump-version.ts prerelease --prerelease-tag preview" + if [ -n "$CHANGELOG_INPUT" ]; then + BUMP_CMD="$BUMP_CMD --changelog \"$CHANGELOG_INPUT\"" + fi + eval $BUMP_CMD + + NEW_VERSION=$(node -p "require('./package.json').version") + echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "branch=release/v$NEW_VERSION" >> $GITHUB_OUTPUT + echo "📦 Preview version: $NEW_VERSION" + + - name: Regenerate JSON schema + run: | + npm run build + node scripts/generate-schema.mjs + npx prettier --write schemas/ + + - name: Create release branch and PR + env: + GH_TOKEN: ${{ github.token }} + NEW_VERSION: ${{ steps.bump.outputs.version }} + run: | + BRANCH_NAME="release/v$NEW_VERSION" + git ls-remote --exit-code --heads origin $BRANCH_NAME && git push origin --delete $BRANCH_NAME || true + git show-ref --verify --quiet refs/heads/$BRANCH_NAME && git branch -D $BRANCH_NAME || true + + git checkout -b $BRANCH_NAME + git add -A + git commit -m "chore: bump version to $NEW_VERSION" + git push origin $BRANCH_NAME + + gh pr create \ + --base preview \ + --head "$BRANCH_NAME" \ + --title "Release v$NEW_VERSION (preview)" \ + --body "## Release v$NEW_VERSION (preview) + + Part of a coordinated main + preview release. + + ### Checklist + - [ ] Review CHANGELOG.md + - [ ] All CI checks passing + - [ ] Merge this PR before approving the publish step" + + # ═══════════════════════════════════════════════════════════════════ + # Step 3 — Build and test both + # ═══════════════════════════════════════════════════════════════════ + test-main: + name: Test Main + needs: prepare-main + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: release/v${{ needs.prepare-main.outputs.version }} + - uses: actions/setup-node@v6 + with: + node-version: 20.x + - name: Configure git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + - run: curl -LsSf https://astral.sh/uv/install.sh | sh + - run: npm ci + - run: npm run lint + - run: npm run typecheck + - run: npm run build + - run: npm run test:unit + + test-preview: + name: Test Preview + needs: prepare-preview + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + ref: release/v${{ needs.prepare-preview.outputs.version }} + - uses: actions/setup-node@v6 + with: + node-version: 20.x + - name: Configure git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + - run: curl -LsSf https://astral.sh/uv/install.sh | sh + - run: npm ci + - run: npm run lint + - run: npm run typecheck + - run: npm run build + - run: npm run test:unit + + # ═══════════════════════════════════════════════════════════════════ + # Step 4 — Manual approval gate + # ═══════════════════════════════════════════════════════════════════ + release-approval: + name: Release Approval (Both) + needs: [test-main, test-preview, prepare-main, prepare-preview] + runs-on: ubuntu-latest + environment: + name: npm-publish-approval + steps: + - name: Approval checkpoint + env: + MAIN_VERSION: ${{ needs.prepare-main.outputs.version }} + PREVIEW_VERSION: ${{ needs.prepare-preview.outputs.version }} + run: | + echo "✅ Both builds and tests passed" + echo "" + echo "📦 Main version: $MAIN_VERSION (npm tag: latest)" + echo "📦 Preview version: $PREVIEW_VERSION (npm tag: preview)" + echo "" + echo "⚠️ MANUAL APPROVAL REQUIRED" + echo "" + echo "Before approving:" + echo "1. Merge the main release PR (release/v$MAIN_VERSION → main)" + echo "2. Merge the preview release PR (release/v$PREVIEW_VERSION → preview)" + echo "3. Verify both PRs are merged" + + # ═══════════════════════════════════════════════════════════════════ + # Step 5 — Verify both PRs merged before any publish + # ═══════════════════════════════════════════════════════════════════ + verify-merges: + name: Verify Both PRs Merged + needs: [prepare-main, prepare-preview, release-approval] + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Verify main version + env: + EXPECTED: ${{ needs.prepare-main.outputs.version }} + run: | + git fetch origin main + ACTUAL=$(git show origin/main:package.json | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).version") + if [ "$ACTUAL" != "$EXPECTED" ]; then + echo "❌ Main release PR not merged yet!" + echo "Expected: $EXPECTED, Got: $ACTUAL" + exit 1 + fi + echo "✅ Main version verified: $ACTUAL" + + - name: Verify preview version + env: + EXPECTED: ${{ needs.prepare-preview.outputs.version }} + run: | + git fetch origin preview + ACTUAL=$(git show origin/preview:package.json | node -p "JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')).version") + if [ "$ACTUAL" != "$EXPECTED" ]; then + echo "❌ Preview release PR not merged yet!" + echo "Expected: $EXPECTED, Got: $ACTUAL" + exit 1 + fi + echo "✅ Preview version verified: $ACTUAL" + + # ═══════════════════════════════════════════════════════════════════ + # Step 6a — Publish main to npm (tag: latest) + # ═══════════════════════════════════════════════════════════════════ + publish-main: + name: Publish Main (@latest) + needs: [prepare-main, verify-merges] + runs-on: ubuntu-latest + environment: + name: npm-publish + url: https://www.npmjs.com/package/@aws/agentcore + permissions: + id-token: write + contents: write + + steps: + - name: Checkout main + uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version: 22.x + registry-url: 'https://registry.npmjs.org' + + - run: npm install -g npm@11.5.1 + - run: npm ci + - run: npm run build + + - name: Publish to npm + run: npm publish --access public --provenance --tag latest + + - name: Tag and release + env: + VERSION: ${{ needs.prepare-main.outputs.version }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "v$VERSION" -m "Release v$VERSION" + git push origin "v$VERSION" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.prepare-main.outputs.version }} + name: AgentCore CLI v${{ needs.prepare-main.outputs.version }} + generate_release_notes: true + prerelease: false + body: | + ## Installation + + ```bash + npm install -g @aws/agentcore@${{ needs.prepare-main.outputs.version }} + ``` + + # ═══════════════════════════════════════════════════════════════════ + # Step 6b — Publish preview to npm (tag: preview) + # ═══════════════════════════════════════════════════════════════════ + publish-preview: + name: Publish Preview (@preview) + needs: [prepare-preview, verify-merges] + runs-on: ubuntu-latest + environment: + name: npm-publish + url: https://www.npmjs.com/package/@aws/agentcore + permissions: + id-token: write + contents: write + + steps: + - name: Checkout preview + uses: actions/checkout@v6 + with: + ref: preview + fetch-depth: 0 + + - uses: actions/setup-node@v6 + with: + node-version: 22.x + registry-url: 'https://registry.npmjs.org' + + - run: npm install -g npm@11.5.1 + - run: npm ci + - run: npm run build + + - name: Publish to npm + run: npm publish --access public --provenance --tag preview + + - name: Tag and release + env: + VERSION: ${{ needs.prepare-preview.outputs.version }} + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + git tag -a "v$VERSION" -m "Release v$VERSION (preview)" + git push origin "v$VERSION" + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: v${{ needs.prepare-preview.outputs.version }} + name: AgentCore CLI v${{ needs.prepare-preview.outputs.version }} (Preview) + generate_release_notes: true + prerelease: true + body: | + ## Installation (Preview) + + ```bash + npm install -g @aws/agentcore@preview + ``` + + # ═══════════════════════════════════════════════════════════════════ + # Summary + # ═══════════════════════════════════════════════════════════════════ + summary: + name: Release Summary + needs: [prepare-main, prepare-preview, publish-main, publish-preview] + if: always() + runs-on: ubuntu-latest + steps: + - name: Summary + env: + MAIN_VERSION: ${{ needs.prepare-main.outputs.version }} + PREVIEW_VERSION: ${{ needs.prepare-preview.outputs.version }} + MAIN_STATUS: ${{ needs.publish-main.result }} + PREVIEW_STATUS: ${{ needs.publish-preview.result }} + run: | + echo "## Release Summary" + echo "" + echo "| Package | Version | npm Tag | Status |" + echo "|---------|---------|---------|--------|" + echo "| @aws/agentcore | $MAIN_VERSION | latest | $MAIN_STATUS |" + echo "| @aws/agentcore | $PREVIEW_VERSION | preview | $PREVIEW_STATUS |" diff --git a/.github/workflows/sync-preview.yml b/.github/workflows/sync-preview.yml new file mode 100644 index 000000000..61abcbc86 --- /dev/null +++ b/.github/workflows/sync-preview.yml @@ -0,0 +1,125 @@ +name: Sync Preview with Main + +on: + push: + branches: [main] + +concurrency: + group: sync-preview + cancel-in-progress: false + +permissions: + contents: write + pull-requests: write + +jobs: + sync: + name: Merge main into preview + runs-on: ubuntu-latest + steps: + - name: Checkout preview + uses: actions/checkout@v6 + with: + ref: preview + fetch-depth: 0 + + - name: Configure git + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "github-actions[bot]@users.noreply.github.com" + + - name: Merge main into preview + id: merge + run: | + git fetch origin main + MAIN_SHA=$(git rev-parse origin/main) + MERGE_BASE=$(git merge-base HEAD origin/main) + + if [[ "$MAIN_SHA" == "$MERGE_BASE" ]]; then + echo "✅ preview already contains all of main" + echo "status=up-to-date" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "ℹ️ Merging main into preview..." + + if git merge origin/main --no-edit -m "chore: merge main into preview"; then + git push origin preview + echo "✅ main merged into preview and pushed" + echo "status=merged" >> $GITHUB_OUTPUT + else + git merge --abort + echo "status=conflict" >> $GITHUB_OUTPUT + fi + + - name: Get original commit author + if: steps.merge.outputs.status == 'conflict' + id: author + run: | + AUTHOR=$(git log origin/main -1 --format='%an') + GH_USER=$(git log origin/main -1 --format='%ae' | grep -oP '.*(?=@users\.noreply\.github\.com)' || echo "") + if [[ -z "$GH_USER" ]]; then + # Try to get GitHub username from the commit + GH_USER=$(gh api "/repos/${{ github.repository }}/commits/$(git rev-parse origin/main)" --jq '.author.login // empty' 2>/dev/null || echo "") + fi + echo "name=$AUTHOR" >> $GITHUB_OUTPUT + echo "gh_user=$GH_USER" >> $GITHUB_OUTPUT + env: + GH_TOKEN: ${{ github.token }} + + - name: Create PR for conflict resolution + if: steps.merge.outputs.status == 'conflict' + env: + GH_TOKEN: ${{ github.token }} + AUTHOR_NAME: ${{ steps.author.outputs.name }} + AUTHOR_GH: ${{ steps.author.outputs.gh_user }} + run: | + BRANCH="sync-preview/merge-main-$(date +%Y%m%d-%H%M%S)" + + # Check if there's already an open sync PR + EXISTING=$(gh pr list --base preview --search "sync-preview: merge main into preview" --state open --json number --jq 'length') + if [[ "$EXISTING" != "0" ]]; then + echo "ℹ️ Sync PR already open — skipping duplicate." + exit 0 + fi + + # Create a branch from preview with the conflict markers + git checkout -b "$BRANCH" + git merge origin/main --no-edit -m "chore: merge main into preview" || true + git add -A + git commit --no-edit -m "chore: merge main into preview (conflicts need resolution)" || true + git push origin "$BRANCH" + + # Build mention string + MENTION="" + if [[ -n "$AUTHOR_GH" ]]; then + MENTION="cc @${AUTHOR_GH}" + fi + + gh pr create \ + --base preview \ + --head "$BRANCH" \ + --title "sync-preview: merge main into preview" \ + --body "$(cat < + \`\`\` + 2. Search for conflict markers and resolve them: + \`\`\`bash + grep -rn '<<<<<<< HEAD' . + \`\`\` + 3. Keep preview-specific values (package version, preview tests, etc.) — accept main's changes for everything else. + 4. Commit and push, then merge this PR. + + This must be resolved before the next coordinated release. + + ${MENTION} + + _Opened automatically by the sync-preview workflow._ + BODY + )" From 1fc366c9bd9fa211cd385d9b1596a341c9e5af65 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:04:38 -0400 Subject: [PATCH 05/45] chore(deps): bump the aws-sdk group with 14 updates (#1024) Bumps the aws-sdk group with 14 updates: | Package | From | To | | --- | --- | --- | | [@aws-sdk/client-application-signals](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-application-signals) | `3.1037.0` | `3.1038.0` | | [@aws-sdk/client-bedrock](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-bedrock) | `3.1037.0` | `3.1038.0` | | [@aws-sdk/client-bedrock-agent](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-bedrock-agent) | `3.1037.0` | `3.1038.0` | | [@aws-sdk/client-bedrock-agentcore](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-bedrock-agentcore) | `3.1037.0` | `3.1038.0` | | [@aws-sdk/client-bedrock-agentcore-control](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-bedrock-agentcore-control) | `3.1037.0` | `3.1038.0` | | [@aws-sdk/client-bedrock-runtime](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-bedrock-runtime) | `3.1037.0` | `3.1038.0` | | [@aws-sdk/client-cloudformation](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-cloudformation) | `3.1037.0` | `3.1038.0` | | [@aws-sdk/client-cloudwatch-logs](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-cloudwatch-logs) | `3.1037.0` | `3.1038.0` | | [@aws-sdk/client-resource-groups-tagging-api](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-resource-groups-tagging-api) | `3.1037.0` | `3.1038.0` | | [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1037.0` | `3.1038.0` | | [@aws-sdk/client-sts](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-sts) | `3.1037.0` | `3.1038.0` | | [@aws-sdk/client-xray](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-xray) | `3.1037.0` | `3.1038.0` | | [@aws-sdk/credential-providers](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/packages/credential-providers) | `3.1037.0` | `3.1038.0` | | [@aws-sdk/client-cognito-identity-provider](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-cognito-identity-provider) | `3.1037.0` | `3.1038.0` | Updates `@aws-sdk/client-application-signals` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-application-signals/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/clients/client-application-signals) Updates `@aws-sdk/client-bedrock` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-bedrock/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/clients/client-bedrock) Updates `@aws-sdk/client-bedrock-agent` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-bedrock-agent/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/clients/client-bedrock-agent) Updates `@aws-sdk/client-bedrock-agentcore` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-bedrock-agentcore/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/clients/client-bedrock-agentcore) Updates `@aws-sdk/client-bedrock-agentcore-control` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-bedrock-agentcore-control/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/clients/client-bedrock-agentcore-control) Updates `@aws-sdk/client-bedrock-runtime` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-bedrock-runtime/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/clients/client-bedrock-runtime) Updates `@aws-sdk/client-cloudformation` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-cloudformation/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/clients/client-cloudformation) Updates `@aws-sdk/client-cloudwatch-logs` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-cloudwatch-logs/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/clients/client-cloudwatch-logs) Updates `@aws-sdk/client-resource-groups-tagging-api` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-resource-groups-tagging-api/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/clients/client-resource-groups-tagging-api) Updates `@aws-sdk/client-s3` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/clients/client-s3) Updates `@aws-sdk/client-sts` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-sts/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/clients/client-sts) Updates `@aws-sdk/client-xray` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-xray/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/clients/client-xray) Updates `@aws-sdk/credential-providers` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/packages/credential-providers/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/packages/credential-providers) Updates `@aws-sdk/client-cognito-identity-provider` from 3.1037.0 to 3.1038.0 - [Release notes](https://github.com/aws/aws-sdk-js-v3/releases) - [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-cognito-identity-provider/CHANGELOG.md) - [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1038.0/clients/client-cognito-identity-provider) --- updated-dependencies: - dependency-name: "@aws-sdk/client-application-signals" dependency-version: 3.1038.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/client-bedrock" dependency-version: 3.1038.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/client-bedrock-agent" dependency-version: 3.1038.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/client-bedrock-agentcore" dependency-version: 3.1038.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/client-bedrock-agentcore-control" dependency-version: 3.1038.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/client-bedrock-runtime" dependency-version: 3.1038.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/client-cloudformation" dependency-version: 3.1038.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/client-cloudwatch-logs" dependency-version: 3.1038.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/client-resource-groups-tagging-api" dependency-version: 3.1038.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/client-s3" dependency-version: 3.1038.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/client-sts" dependency-version: 3.1038.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/client-xray" dependency-version: 3.1038.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/credential-providers" dependency-version: 3.1038.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-sdk - dependency-name: "@aws-sdk/client-cognito-identity-provider" dependency-version: 3.1038.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: aws-sdk ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 550 ++++++++++++++++++++++------------------------ 1 file changed, 257 insertions(+), 293 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7497a04fa..7b294f07a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -687,24 +687,24 @@ } }, "node_modules/@aws-sdk/client-application-signals": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-application-signals/-/client-application-signals-3.1037.0.tgz", - "integrity": "sha512-xnGVyIWU1SXNSnnARvU3U3sic0QWH0wek/X3WpXFCpOm0NBzbTablWiAszNDU9RCvg9KUDm6Wdp0T4jnodXhEg==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-application-signals/-/client-application-signals-3.1038.0.tgz", + "integrity": "sha512-kUBaXESTxZO+JiMQIFU/Wv0Lop+KtuJP0xOv00dL/4a0RdcqNdAwbH7RnxXaL5z7toDLCfg9j93f0IQEdBLbXA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -712,7 +712,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -728,7 +728,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -788,25 +788,25 @@ } }, "node_modules/@aws-sdk/client-bedrock": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock/-/client-bedrock-3.1037.0.tgz", - "integrity": "sha512-XGuJ86vuuEsqp0Gq8fMCSMd/VNCwqTvKwFT99SU2OOLyNp31ChZ+LdIckJZl/A3jpUyZYpXjn7IxP/N/6UFiZA==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock/-/client-bedrock-3.1038.0.tgz", + "integrity": "sha512-WY99Vodg7V4hxLQn7HOLawXHeVYv8Ys16Xx3CPpu8L7+1spvO/i4uykzTXH6GkojdAqNO2CSclhk31lb85nSWg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/token-providers": "3.1037.0", + "@aws-sdk/token-providers": "3.1038.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -814,7 +814,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -830,7 +830,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -839,24 +839,24 @@ } }, "node_modules/@aws-sdk/client-bedrock-agent": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agent/-/client-bedrock-agent-3.1037.0.tgz", - "integrity": "sha512-8Uc3zdwfxmjMrXb4qP69bByL054R+jWNaW3/Hq93E1jnag8vZz8YhJlz1x6jr1oXOoOLVErc1L/g8x0sMFZuRA==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agent/-/client-bedrock-agent-3.1038.0.tgz", + "integrity": "sha512-cwOkwVhYs+GeW7SzNS/pmgkHZ/SAyv4wIkz62vew6sM6R+XgqqBbtEqDrf+FSsprFtx75edbduRg8boJt0veSg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -864,7 +864,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -880,7 +880,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -889,24 +889,24 @@ } }, "node_modules/@aws-sdk/client-bedrock-agentcore": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agentcore/-/client-bedrock-agentcore-3.1037.0.tgz", - "integrity": "sha512-8WmZulMmFnCWFuX2rDBoZdebCMmmrAi1VABsLgm4O73w3+s7tcON1YgspG9gTevuVRtOVdk1B6TLw2Mo8NBHSQ==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agentcore/-/client-bedrock-agentcore-3.1038.0.tgz", + "integrity": "sha512-HHsWj3AWsBnvihV0oUolOlnPxXum75kTdQHuYMX6kko4ek2iQWOe1WxUQG4TwOS7HUivU3Zf6CNti/K6wEkc7A==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", @@ -917,7 +917,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -933,7 +933,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -943,24 +943,24 @@ } }, "node_modules/@aws-sdk/client-bedrock-agentcore-control": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agentcore-control/-/client-bedrock-agentcore-control-3.1037.0.tgz", - "integrity": "sha512-tMfeMgohJ6L9ARRSdK8O7lbdYIggeRXtuRQFS+kISZTlvw+L4TjhUZ7TT5yIBO0UVkunoWsRQIH4VP4pgiVqQA==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-agentcore-control/-/client-bedrock-agentcore-control-3.1038.0.tgz", + "integrity": "sha512-S+BtFwV7C5yUwjMZwExp0hAmdMCXLA2iG6KPvoS4zv8T2bMk1itrWfE4opqSZ1lgwDmnazcG7mmfAAUBFXvKXw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -968,7 +968,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -984,9 +984,9 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.16", + "@smithy/util-waiter": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -994,28 +994,28 @@ } }, "node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1037.0.tgz", - "integrity": "sha512-Evla4DUdBf1pQpQa7pbfquj7jRaRktkI0qGoWBJBXWB9wQISzJ8OEI4sHugk/W6SF47C7hMP/o3Z/XBrfnejCw==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1038.0.tgz", + "integrity": "sha512-oGiqs9v9WzPOdv7PDdm9iPibHgrbDvCDyNg43wFZn2PiiEUisFM+xUP2CRMsj41SmwZPhohmZkXiUu1+MghbAQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/eventstream-handler-node": "^3.972.14", "@aws-sdk/middleware-eventstream": "^3.972.10", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/middleware-websocket": "^3.972.16", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/token-providers": "3.1037.0", + "@aws-sdk/token-providers": "3.1038.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", @@ -1026,7 +1026,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1042,7 +1042,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" @@ -1051,42 +1051,6 @@ "node": ">=20.0.0" } }, - "node_modules/@aws-sdk/client-bedrock-runtime/node_modules/@aws-sdk/token-providers": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1037.0.tgz", - "integrity": "sha512-csxa484KboWLs3f8jFQ5v9RwH8FVf0fQ+SO3GSXyu4Jtinhh4qXmOWLSVX30RBpB933dZaKGHGEXzEEY88NqRw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-bedrock/node_modules/@aws-sdk/token-providers": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1037.0.tgz", - "integrity": "sha512-csxa484KboWLs3f8jFQ5v9RwH8FVf0fQ+SO3GSXyu4Jtinhh4qXmOWLSVX30RBpB933dZaKGHGEXzEEY88NqRw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", - "@aws-sdk/types": "^3.973.8", - "@smithy/property-provider": "^4.2.14", - "@smithy/shared-ini-file-loader": "^4.4.9", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, "node_modules/@aws-sdk/client-cloudcontrol": { "version": "3.1036.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudcontrol/-/client-cloudcontrol-3.1036.0.tgz", @@ -1139,24 +1103,24 @@ } }, "node_modules/@aws-sdk/client-cloudformation": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudformation/-/client-cloudformation-3.1037.0.tgz", - "integrity": "sha512-nLSwtmayv7tjjp6t8Lc20xZCeA+XJ5UzXvauQCnO3aRZVAxrgarQntZjS+eWlRYGRqLBjXSre4xL7XwUlObb2A==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudformation/-/client-cloudformation-3.1038.0.tgz", + "integrity": "sha512-5c8u4magoWj8uAhiKoBY0/FXqCRqw3g05RXiQzUdNbeZDhfnv2b/r0aITVLlVEW/zfQUGi/WJfaV4vuffp6k1A==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1164,7 +1128,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1180,9 +1144,9 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.16", + "@smithy/util-waiter": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -1190,24 +1154,24 @@ } }, "node_modules/@aws-sdk/client-cloudwatch-logs": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1037.0.tgz", - "integrity": "sha512-W0TRDfyBikNR+DzOTBgBLT4TqVHCAasqx2Xu4G4PfTRCansUtEJRydq0CEVOpHlMfme4Va89O/r9sp/VoDsKRg==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cloudwatch-logs/-/client-cloudwatch-logs-3.1038.0.tgz", + "integrity": "sha512-OfdF+xZiiHWR5JL9rsc8jjlk1fNxnX2uS2iWWBM9vncVSXdhPAMb/rFNeHLkIx9IfmyiNi7kXUTAKgIUkVN0+w==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", @@ -1218,7 +1182,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1234,7 +1198,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1293,24 +1257,24 @@ } }, "node_modules/@aws-sdk/client-cognito-identity": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1037.0.tgz", - "integrity": "sha512-/BQAyz98JRQFg3E8de3fGGydIYnsFRd6Cla4+zkviOe641fLCG0ZkPIk9D22HSi8qy9XKx+zk6ed2PcLO8uuPw==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity/-/client-cognito-identity-3.1038.0.tgz", + "integrity": "sha512-tTSXUZXzydM0VUoxcrM4YrhhQfFgepfpbRLEq460650rFAC8NsGhGQ6Ixo7UPV6TKEyI/jQcCnQVi4RVM4SkAg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1318,7 +1282,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1334,7 +1298,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1343,25 +1307,25 @@ } }, "node_modules/@aws-sdk/client-cognito-identity-provider": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity-provider/-/client-cognito-identity-provider-3.1037.0.tgz", - "integrity": "sha512-w0HuaMNtzcj6bErBX8/TVGbOz0a8JNCzPHLMq2u/ll4uuxl9Xut0njuy7vyY0/pYCuNE+no4uq+yHwn8U3ptgw==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-cognito-identity-provider/-/client-cognito-identity-provider-3.1038.0.tgz", + "integrity": "sha512-E3/2sei5wiUvS+ZdlvQ93SXU2C9zdqq1mJqxik6B8GHtSnP2T4J97Hdu46FM6GK/HZJ+iFZFeIlM0B+P3uH2QQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1369,7 +1333,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1385,7 +1349,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1755,24 +1719,24 @@ } }, "node_modules/@aws-sdk/client-resource-groups-tagging-api": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-resource-groups-tagging-api/-/client-resource-groups-tagging-api-3.1037.0.tgz", - "integrity": "sha512-1+sv7vbSSRqVMBTvgFPFopXZ/SGdz3jP9aIHM8eOIuuZGYSmuqiXkNd7Ag5yxIQqEDO8sASevCqscEqN0f4OUw==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-resource-groups-tagging-api/-/client-resource-groups-tagging-api-3.1038.0.tgz", + "integrity": "sha512-jEjVlhcAb4j658XKfxBPyyrbXJKmfg2bR3Xokqy5lhvZA80t6p47jeQ4s5yXIsaNzxnefJ8SuqrVft2KBHbDrQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -1780,7 +1744,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1796,7 +1760,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -1857,32 +1821,32 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1037.0.tgz", - "integrity": "sha512-DBmA1jAW8ST6C4srBxeL1/RLIir/d8WOm4s4mi59mGp6mBktHM59Kwb7GuURaCO60cotuce5zr0sKpMLPcBQyA==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1038.0.tgz", + "integrity": "sha512-k60qm50bWkaqNfCJe1z28WaqgpztE0wbWVMZw6ZJcTOGfrWFhsJeLCEqtkH8w00iEozKx9GQwdQXz4G0sMGdKA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-bucket-endpoint": "^3.972.10", "@aws-sdk/middleware-expect-continue": "^3.972.10", - "@aws-sdk/middleware-flexible-checksums": "^3.974.13", + "@aws-sdk/middleware-flexible-checksums": "^3.974.14", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-location-constraint": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-sdk-s3": "^3.972.34", + "@aws-sdk/middleware-sdk-s3": "^3.972.35", "@aws-sdk/middleware-ssec": "^3.972.10", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.22", + "@aws-sdk/signature-v4-multi-region": "^3.996.23", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/eventstream-serde-browser": "^4.2.14", @@ -1896,7 +1860,7 @@ "@smithy/md5-js": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -1912,10 +1876,10 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-stream": "^4.5.25", "@smithy/util-utf8": "^4.2.2", - "@smithy/util-waiter": "^4.2.16", + "@smithy/util-waiter": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -2074,25 +2038,25 @@ } }, "node_modules/@aws-sdk/client-sts": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.1037.0.tgz", - "integrity": "sha512-Ye+BEvy1Fd/JtqfF1T9PiodIU52/Cd9sP4oBLnj8QQEyYRUcYG1OQ2xIFXF/gzAAMjfVN8HqGJo9LxdmScxZAQ==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.1038.0.tgz", + "integrity": "sha512-lboBAXDIr+ot5a357mQgaAwgMMYZW7EwO216LTASUHV3UN4YgqskrEcwsDV9765KH9wUDGxFt8rClS4ixaOgiA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.22", + "@aws-sdk/signature-v4-multi-region": "^3.996.23", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -2100,7 +2064,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -2116,7 +2080,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -2125,24 +2089,24 @@ } }, "node_modules/@aws-sdk/client-xray": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-xray/-/client-xray-3.1037.0.tgz", - "integrity": "sha512-cnic610qpFrbR3gNw0pFi6EFrkDhDJOHlOnQzpdjBQ38O3QXwkON6Kco8IyTs7TlyOr7HRmHnBiEYVDZrLVC0w==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-xray/-/client-xray-3.1038.0.tgz", + "integrity": "sha512-wPO92YHLQDbaHog82RFCmrGmMl5bdF6SOcejBr/RGcORsL4t6RXvaVP9sF73txgOTDRgUQh/EO1uy/JmTC9Dgg==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-node": "^3.972.36", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-node": "^3.972.37", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -2150,7 +2114,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -2166,7 +2130,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -2175,13 +2139,13 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.974.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.5.tgz", - "integrity": "sha512-lMPlYlYfQdNZhlkJgnkmESwrY+hNh3PljmZ+37oAqLNdJ6rnILAwFSyc6B3bJeDOtMORNnMQIej0aTRuOlDyhQ==", + "version": "3.974.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.6.tgz", + "integrity": "sha512-8Vu7zGxu+39ChR/s5J7nXBw3a2kMHAi0OfKT8ohgTVjX0qYed/8mIfdBb638oBmKrWCwwKjYAM5J/4gMJ8nAJA==", "license": "Apache-2.0", "dependencies": { "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.19", + "@aws-sdk/xml-builder": "^3.972.20", "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/property-provider": "^4.2.14", @@ -2191,7 +2155,7 @@ "@smithy/types": "^4.14.1", "@smithy/util-base64": "^4.3.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -2213,12 +2177,12 @@ } }, "node_modules/@aws-sdk/credential-provider-cognito-identity": { - "version": "3.972.28", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.28.tgz", - "integrity": "sha512-UXhc4FfxbfNaIqycDnIZ+W8CMAoCtcJJfZkq+cWSUwQRN0V0d0uAoN2qCFyKZip8inlHeKJmNQsPliKKcElP8Q==", + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-cognito-identity/-/credential-provider-cognito-identity-3.972.29.tgz", + "integrity": "sha512-fklwtMw+9+1TRNa7KOCaaE9P9ubN6PdKCVlviX/vPRNtnMGIivAFrWcYsAcyw+sHPPioiSCSOHKKAhtOkO6IGg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", @@ -2229,12 +2193,12 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.31", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.31.tgz", - "integrity": "sha512-X/yGB73LmDW/6MdDJGCDzZBUXnM3ys4vs9l+5ZTJmiEswDdP1OjeoAFlFjVGS9o4KB2wZWQ9KOfdVNSSK6Ep3w==", + "version": "3.972.32", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.32.tgz", + "integrity": "sha512-7vA4GHg8NSmQxquJHSBcSM3RgB4ZaaRi6u4+zGFKOmOH6aqlgr2Sda46clkZDYzlirgfY96w15Zj0jh6PT48ng==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/types": "^4.14.1", @@ -2245,12 +2209,12 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.33", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.33.tgz", - "integrity": "sha512-c0ZF+lwoWVvX5iCaGKL5T/4DnIw88CGqxA0BcBs3U86mIp5EZYPVg+KSPkMXOyokmADvNewiMUfSG2uFwjRp0g==", + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.34.tgz", + "integrity": "sha512-vBrhWujFCLp1u8ptJRWYlipMutzPptb8pDQ00rKVH9q67T7rGd3VTWIj63aKrlLuY6qSsw1Rt5F/D/7wnNgryA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/types": "^3.973.8", "@smithy/fetch-http-handler": "^5.3.17", "@smithy/node-http-handler": "^4.6.1", @@ -2266,19 +2230,19 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.35.tgz", - "integrity": "sha512-jsU4u/cRkKFLKQS0k918FQ27fzXLG5ENiLWQMYE6581zLeI2hWh04ptlrvZMB3wJT/5d+vSzJk74X1CMFr4y8Q==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.36.tgz", + "integrity": "sha512-FBHyCmV8EB0gUvh1d+CZm87zt2PrdC7OyWexLRoH3I5zWSOUGa+9t58Y5jbxRfwUp3AWpHAFvKY6YzgR845sVA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-env": "^3.972.31", - "@aws-sdk/credential-provider-http": "^3.972.33", - "@aws-sdk/credential-provider-login": "^3.972.35", - "@aws-sdk/credential-provider-process": "^3.972.31", - "@aws-sdk/credential-provider-sso": "^3.972.35", - "@aws-sdk/credential-provider-web-identity": "^3.972.35", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-env": "^3.972.32", + "@aws-sdk/credential-provider-http": "^3.972.34", + "@aws-sdk/credential-provider-login": "^3.972.36", + "@aws-sdk/credential-provider-process": "^3.972.32", + "@aws-sdk/credential-provider-sso": "^3.972.36", + "@aws-sdk/credential-provider-web-identity": "^3.972.36", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", @@ -2291,13 +2255,13 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.35.tgz", - "integrity": "sha512-5oa3j0cA50jPqgNhZ9XdJVopuzUf1klRb28/2MfLYWWiPi9DRVvbrBWT+DidbHTT36520VuXZJahQwR+YgSjrg==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.36.tgz", + "integrity": "sha512-IFap01lJKxQc0C/OHmZwZQr/cKq0DhrcmKedRrdnnl42D+P0SImnnnWQjv07uIPqpEdtqmkPXb9TiPYTU+prxQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/protocol-http": "^5.3.14", @@ -2310,17 +2274,17 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.36", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.36.tgz", - "integrity": "sha512-4nT2T8Z7vH8KE9EdjEsuIlHpZSlcaK2PrKbQBjuUGU46BCCzF3WvP0u0Uiosni3Ykmmn4rWLVawoOCLotUtCbg==", + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.37.tgz", + "integrity": "sha512-/WFixFAAiw8WpmjZcI0l4t3DerXLmVinOIfuotmRZnu2qmsFPoqqmstASz0z8bi1pGdFXzeLzf6bwucM3mZcUQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.31", - "@aws-sdk/credential-provider-http": "^3.972.33", - "@aws-sdk/credential-provider-ini": "^3.972.35", - "@aws-sdk/credential-provider-process": "^3.972.31", - "@aws-sdk/credential-provider-sso": "^3.972.35", - "@aws-sdk/credential-provider-web-identity": "^3.972.35", + "@aws-sdk/credential-provider-env": "^3.972.32", + "@aws-sdk/credential-provider-http": "^3.972.34", + "@aws-sdk/credential-provider-ini": "^3.972.36", + "@aws-sdk/credential-provider-process": "^3.972.32", + "@aws-sdk/credential-provider-sso": "^3.972.36", + "@aws-sdk/credential-provider-web-identity": "^3.972.36", "@aws-sdk/types": "^3.973.8", "@smithy/credential-provider-imds": "^4.2.14", "@smithy/property-provider": "^4.2.14", @@ -2333,12 +2297,12 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.31", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.31.tgz", - "integrity": "sha512-eKeT4MXumpBJsrDLCYcSzIkFPVTFn/es7It2oogp2OhU/ic7P/+xzFpQx9ZhwtXS57Mc5S42BPWi7lHmvs/nYg==", + "version": "3.972.32", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.32.tgz", + "integrity": "sha512-uZp4tlGbpczV8QxmtIwOpSkcyGtBRR8/T4BAumRKfAt1nwCig3FSCZvrKl6ARDIDVRYn5p2oRcAsfFR01EgMGA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -2350,14 +2314,14 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.35.tgz", - "integrity": "sha512-bCuBdfnj0KGDMdLp6utMTLiJcFN2ek9EgZinxQZZSc3FxjJ/HSqeqab2cjbnoNfy8RM6suDCsRkmVY1izp9I+A==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.36.tgz", + "integrity": "sha512-DsLr0UHMyKzRJKe2bjlwU8q1cfoXg8TIJKV/xwvnalAemiZLOZunFzj/whGnFDZIBVLdnbLiwv5SvRf1+CSwkg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", - "@aws-sdk/token-providers": "3.1036.0", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/nested-clients": "^3.997.4", + "@aws-sdk/token-providers": "3.1038.0", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -2369,13 +2333,13 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.35.tgz", - "integrity": "sha512-swW6Bwvl8lanyEMtZOWE/oR6yqcRQH4HTQZUVsnDVgoXvRjRywpYpLv2BWwjUFyjPrqsdX6FeTkf4tMSe/qFTQ==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.36.tgz", + "integrity": "sha512-uzrURO7frJhHQVVNR5zBJcCYeMYflmXcWBK1+MiBym2Dfjh6nXATrMixrmGZi+97Q7ETZ+y/4lUwAy0Nfnznjw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -2387,23 +2351,23 @@ } }, "node_modules/@aws-sdk/credential-providers": { - "version": "3.1037.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1037.0.tgz", - "integrity": "sha512-TPPoQzfNkWltNgjJn3RRY1S8VXffDvv49xGGs9K0DrYS9LZCLLsoHmSmShx9HQusPc/4Oz23rfRWTolCU19PdQ==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-providers/-/credential-providers-3.1038.0.tgz", + "integrity": "sha512-+B9BuRVPPKF0Q6msVS4vUGOsL4eUg7XYogikp56rUEQVoUVxn5ONyWlnNzsDMTv+BwuBgFo5N7gRZtEToAnSgg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-cognito-identity": "3.1037.0", - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/credential-provider-cognito-identity": "^3.972.28", - "@aws-sdk/credential-provider-env": "^3.972.31", - "@aws-sdk/credential-provider-http": "^3.972.33", - "@aws-sdk/credential-provider-ini": "^3.972.35", - "@aws-sdk/credential-provider-login": "^3.972.35", - "@aws-sdk/credential-provider-node": "^3.972.36", - "@aws-sdk/credential-provider-process": "^3.972.31", - "@aws-sdk/credential-provider-sso": "^3.972.35", - "@aws-sdk/credential-provider-web-identity": "^3.972.35", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/client-cognito-identity": "3.1038.0", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/credential-provider-cognito-identity": "^3.972.29", + "@aws-sdk/credential-provider-env": "^3.972.32", + "@aws-sdk/credential-provider-http": "^3.972.34", + "@aws-sdk/credential-provider-ini": "^3.972.36", + "@aws-sdk/credential-provider-login": "^3.972.36", + "@aws-sdk/credential-provider-node": "^3.972.37", + "@aws-sdk/credential-provider-process": "^3.972.32", + "@aws-sdk/credential-provider-sso": "^3.972.36", + "@aws-sdk/credential-provider-web-identity": "^3.972.36", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", @@ -2521,15 +2485,15 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.974.13", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.13.tgz", - "integrity": "sha512-b6QUe2hQX9XsnCzp6mtzVaERhganDKeb8lmGL6pVhr7rRVH9S9keDFW7uKytuuqmcY5943FixoGqn/QL+sbUBA==", + "version": "3.974.14", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.14.tgz", + "integrity": "sha512-mhTO3amGzYv/DQNbbqZo6UkHquBHlEEVRZwXmjeRqLmy1l9z3xCiFzglPL7n9JpVc2DZc9kjaraAn3JQrueZbw==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/crc64-nvme": "^3.972.7", "@aws-sdk/types": "^3.973.8", "@smithy/is-array-buffer": "^4.2.2", @@ -2638,12 +2602,12 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.34", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.34.tgz", - "integrity": "sha512-/UL96JKjsjdodcRRMKl99tLQvK6Oi9ptLC9iU1yiTF/ruaDX0mtBBtnLNZDxIZRJOCVOtB49ed1YaTadqygk8Q==", + "version": "3.972.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.35.tgz", + "integrity": "sha512-lLppaNTAz+wNgLdi4FtHzrlwrGF0ODTnBWHBaFg85SKs0eJ+M+tP5ifrA8f/0lNd+Ak3MC1NGC6RavV3ny4HTg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-arn-parser": "^3.972.3", "@smithy/core": "^3.23.17", @@ -2677,18 +2641,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.35", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.35.tgz", - "integrity": "sha512-hOFWNOjVmOocpRlrU04nYxjMOeoe0Obu5AXEuhB8zblMCPl3cG1hdluQCZERRKFyhMQjwZnDbhSHjoMUjetFGw==", + "version": "3.972.36", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.36.tgz", + "integrity": "sha512-O2beToxguBvrZFFZ+fFgPbbae8MvyIBjQ6lImee4APHEXXNAD5ZJ2ayLF1mb7rsKw86TM81y5czg82bZncjSjg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@smithy/core": "^3.23.17", "@smithy/protocol-http": "^5.3.14", "@smithy/types": "^4.14.1", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "tslib": "^2.6.2" }, "engines": { @@ -2719,24 +2683,24 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.3.tgz", - "integrity": "sha512-SivE6GP228IVgfsrr2c/vqTg95X0Qj39Yw4uIrcddpkUzIltNMoNOR62leHOLhODfjv9K8X2mPTwS69A5kT0nQ==", + "version": "3.997.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.4.tgz", + "integrity": "sha512-4Sf+WY1lMJzXlw5MiyCMe/UzdILCwvuaHThbqMXS6dfh9gZy3No360I42RXquOI/ULUOhWy2HCyU0Fp20fQGPQ==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.5", + "@aws-sdk/core": "^3.974.6", "@aws-sdk/middleware-host-header": "^3.972.10", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.11", - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/region-config-resolver": "^3.972.13", - "@aws-sdk/signature-v4-multi-region": "^3.996.22", + "@aws-sdk/signature-v4-multi-region": "^3.996.23", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.8", "@aws-sdk/util-user-agent-browser": "^3.972.10", - "@aws-sdk/util-user-agent-node": "^3.973.21", + "@aws-sdk/util-user-agent-node": "^3.973.22", "@smithy/config-resolver": "^4.4.17", "@smithy/core": "^3.23.17", "@smithy/fetch-http-handler": "^5.3.17", @@ -2744,7 +2708,7 @@ "@smithy/invalid-dependency": "^4.2.14", "@smithy/middleware-content-length": "^4.2.14", "@smithy/middleware-endpoint": "^4.4.32", - "@smithy/middleware-retry": "^4.5.5", + "@smithy/middleware-retry": "^4.5.6", "@smithy/middleware-serde": "^4.2.20", "@smithy/middleware-stack": "^4.2.14", "@smithy/node-config-provider": "^4.3.14", @@ -2760,7 +2724,7 @@ "@smithy/util-defaults-mode-node": "^4.2.54", "@smithy/util-endpoints": "^3.4.2", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.5", "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, @@ -2785,12 +2749,12 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.22", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.22.tgz", - "integrity": "sha512-/rXhMXteD+BqhFd0nYprAgcZ/KtU+963uftPqd3tiFcFfooHZINXUGtOmo2SQjRVauCTNqIEzkwuSETdZFqTTA==", + "version": "3.996.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.23.tgz", + "integrity": "sha512-wBbys3Y53Ikly556vyADurKpYQHXS7Jjaskbz+Ga9PZCz7PB/9f3VdKbDlz7dqIzn+xwz7L/a6TR4iXcOi8IRw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.34", + "@aws-sdk/middleware-sdk-s3": "^3.972.35", "@aws-sdk/types": "^3.973.8", "@smithy/protocol-http": "^5.3.14", "@smithy/signature-v4": "^5.3.14", @@ -2802,13 +2766,13 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.1036.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1036.0.tgz", - "integrity": "sha512-aNSJ6jjDYayxN9ZA1JpycVScX93Lx03kKZ1EXt3DGOTahcWVLJj3oLAlop0xKP+vP2Ga2t49p1tEaMkTbCCaZA==", + "version": "3.1038.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1038.0.tgz", + "integrity": "sha512-Qniru+9oGGb/HNK/gGZWbV3jsD0k71ngE7qMQ/x6gYNYLd2EOwHCS6E2E6jfkaqO4i0d+nNKmfRy8bNcshKdGQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.974.5", - "@aws-sdk/nested-clients": "^3.997.3", + "@aws-sdk/core": "^3.974.6", + "@aws-sdk/nested-clients": "^3.997.4", "@aws-sdk/types": "^3.973.8", "@smithy/property-provider": "^4.2.14", "@smithy/shared-ini-file-loader": "^4.4.9", @@ -2900,12 +2864,12 @@ } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.973.21", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.21.tgz", - "integrity": "sha512-Av4UHTcAWgdvbN0IP9pbtf4Qa1+6LtJqQdZWj5pLn5J67w0pnJJAZZ+7JPPcj2KN3378zD2JDM9DwJKEyvyMTQ==", + "version": "3.973.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.22.tgz", + "integrity": "sha512-YTYqTmOUrwbm1h99Ee4y/mVYpFRl0oSO/amtP5cc1BZZWdaAVWs9zj3TkyRHWvR9aI/ZS8m3mS6awXtYUlWyaw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.35", + "@aws-sdk/middleware-user-agent": "^3.972.36", "@aws-sdk/types": "^3.973.8", "@smithy/node-config-provider": "^4.3.14", "@smithy/types": "^4.14.1", @@ -5393,19 +5357,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.5.5", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.5.tgz", - "integrity": "sha512-wnYOpB5vATFKWrY2Z9Alb0KhjZI6AbzU6Fbz3Hq2GnURdRYWB4q+qWivQtSTwXcmWUA3MZ6krfwL6Cq5MAbxsA==", + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.7.tgz", + "integrity": "sha512-bRt6ZImqVSeTk39Nm81K20ObIiAZ3WefY7G6+iz/0tZjs4dgRRjvRX2sgsH+zi6iDCRR/aQvQofLKxxz4rPBZg==", "license": "Apache-2.0", "dependencies": { "@smithy/core": "^3.23.17", "@smithy/node-config-provider": "^4.3.14", "@smithy/protocol-http": "^5.3.14", - "@smithy/service-error-classification": "^4.3.0", + "@smithy/service-error-classification": "^4.3.1", "@smithy/smithy-client": "^4.12.13", "@smithy/types": "^4.14.1", "@smithy/util-middleware": "^4.2.14", - "@smithy/util-retry": "^4.3.4", + "@smithy/util-retry": "^4.3.6", "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, @@ -5525,9 +5489,9 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.0.tgz", - "integrity": "sha512-9jKsBYQRPR0xBLgc2415RsA5PIcP2sis4oBdN9s0D13cg1B1284mNTjx9Yc+BEERXzuPm5ObktI96OxsKh8E9A==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.3.1.tgz", + "integrity": "sha512-aUQuDGh760ts/8MU+APjIZhlLPKhIIfqyzZaJikLEIMrdxFvxuLYD0WxWzaYWpmLbQlXDe9p7EWM3HsBe0K6Gw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1" @@ -5748,12 +5712,12 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.4.tgz", - "integrity": "sha512-FY1UQQ1VFmMwiYp1GVS4MeaGD5O0blLNYK0xCRHU+mJgeoH/hSY8Ld8sJWKQ6uznkh14HveRGQJncgPyNl9J+A==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.6.tgz", + "integrity": "sha512-p6/FO1n2KxMeQyna067i0uJ6TSbb165ZhnRtCpWh4Foxqbfc6oW+XITaL8QkFJj3KFnDe2URt4gOhgU06EP9ew==", "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.3.0", + "@smithy/service-error-classification": "^4.3.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" }, @@ -5806,9 +5770,9 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.16", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.16.tgz", - "integrity": "sha512-GtclrKoZ3Lt7jPQ7aTIYKfjY92OgceScftVnkTsG8e1KV8rkvZgN+ny6YSRhd9hxB8rZtwVbmln7NTvE5O3GmQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.3.0.tgz", + "integrity": "sha512-JyjYmLAfS+pdxF92o4yLgEoy0zhayKTw73FU1aofLWwLcJw7iSqIY2exGmMTrl/lmZugP5p/zxdFSippJDfKWA==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.14.1", From 1686e4daad78772cc5475d2f8a20a0e9e21e6548 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:04:55 -0400 Subject: [PATCH 06/45] chore(deps): bump the aws-cdk group with 2 updates (#1025) Bumps the aws-cdk group with 2 updates: [@aws-cdk/toolkit-lib](https://github.com/aws/aws-cdk-cli/tree/HEAD/packages/@aws-cdk/toolkit-lib) and [aws-cdk-lib](https://github.com/aws/aws-cdk/tree/HEAD/packages/aws-cdk-lib). Updates `@aws-cdk/toolkit-lib` from 1.24.0 to 1.25.0 - [Release notes](https://github.com/aws/aws-cdk-cli/releases) - [Commits](https://github.com/aws/aws-cdk-cli/commits/@aws-cdk/toolkit-lib@v1.25.0/packages/@aws-cdk/toolkit-lib) Updates `aws-cdk-lib` from 2.250.0 to 2.251.0 - [Release notes](https://github.com/aws/aws-cdk/releases) - [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.alpha.md) - [Commits](https://github.com/aws/aws-cdk/commits/v2.251.0/packages/aws-cdk-lib) --- updated-dependencies: - dependency-name: "@aws-cdk/toolkit-lib" dependency-version: 1.25.0 dependency-type: direct:production update-type: version-update:semver-minor dependency-group: aws-cdk - dependency-name: aws-cdk-lib dependency-version: 2.251.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: aws-cdk ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7b294f07a..45a727fa7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -242,9 +242,9 @@ } }, "node_modules/@aws-cdk/cloud-assembly-schema": { - "version": "53.18.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.18.0.tgz", - "integrity": "sha512-/fa6rOpokkfa5tVIdhsaexQq5MVVTSsZSD1Tu45YcrdyGRusGrM9RlPMCPrwvMS1UfdVFBhcgO9dl9ODWAWOeQ==", + "version": "53.19.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.19.0.tgz", + "integrity": "sha512-pHtEeuiPwYu4vWJhqTqBCvOWy2w4BjYFW5W8L5lH5AzhKLLG92o+Ck0oLcKUkB0Ucl4ff49WL9K7sa7g35ionw==", "bundleDependencies": [ "jsonschema", "semver" @@ -377,14 +377,14 @@ } }, "node_modules/@aws-cdk/toolkit-lib": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/@aws-cdk/toolkit-lib/-/toolkit-lib-1.24.0.tgz", - "integrity": "sha512-tgtH0CJ8/N/CpT1/ebOBfUpxdAMSRsP9LTAjWfa+E0clX4Vuvx0w1J1bGYwtvKY9nQUbFIO4QfgNEHz8hVlMUA==", + "version": "1.25.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/toolkit-lib/-/toolkit-lib-1.25.0.tgz", + "integrity": "sha512-yTxQLcI7T8wfUAhWw8vO7dG//x0WL786AA5u4i/yRtSVSJd+0frHX6gD+njSv41OaNLhwP6EVYkFz9wTyCt64A==", "license": "Apache-2.0", "dependencies": { "@aws-cdk/cdk-assets-lib": "^1", "@aws-cdk/cloud-assembly-api": "2.2.2", - "@aws-cdk/cloud-assembly-schema": ">=53.18.0", + "@aws-cdk/cloud-assembly-schema": ">=53.19.0", "@aws-cdk/cloudformation-diff": "^2", "@aws-cdk/cx-api": "^2", "@aws-sdk/client-appsync": "^3", @@ -415,7 +415,7 @@ "@smithy/util-retry": "^4", "@smithy/util-waiter": "^4", "archiver": "^7.0.1", - "cdk-from-cfn": "^0.295.0", + "cdk-from-cfn": "^0.297.0", "chalk": "^4", "chokidar": "^4", "fast-deep-equal": "^3.1.3", @@ -7118,9 +7118,9 @@ } }, "node_modules/aws-cdk-lib": { - "version": "2.250.0", - "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.250.0.tgz", - "integrity": "sha512-8U8/S9VcmKSc3MHZWiB7P0IecgXoohI8Ya3dgtZMgbzC4mB+MEQmsYBeNgm4vzGQdRos8HjQLnFX1IBlZh7jQA==", + "version": "2.251.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.251.0.tgz", + "integrity": "sha512-H1Jfz2Oyejn+yG24i+By9fZpYfg+E3h1XnFCF2wnt/MyGOTIePRph7MRGkX73ap10ERSpmd0Ly58OLVykFoSQA==", "bundleDependencies": [ "@balena/dockerignore", "@aws-cdk/cloud-assembly-api", @@ -7140,8 +7140,8 @@ "dependencies": { "@aws-cdk/asset-awscli-v1": "2.2.273", "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1", - "@aws-cdk/cloud-assembly-api": "^2.2.0", - "@aws-cdk/cloud-assembly-schema": "^53.0.0", + "@aws-cdk/cloud-assembly-api": "^2.2.2", + "@aws-cdk/cloud-assembly-schema": "^53.18.0", "@balena/dockerignore": "^1.0.2", "case": "1.6.3", "fs-extra": "^11.3.3", @@ -7162,7 +7162,7 @@ } }, "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { - "version": "2.2.0", + "version": "2.2.2", "bundleDependencies": [ "jsonschema", "semver" @@ -7178,7 +7178,7 @@ "node": ">= 18.0.0" }, "peerDependencies": { - "@aws-cdk/cloud-assembly-schema": ">=53.0.0" + "@aws-cdk/cloud-assembly-schema": ">=53.15.0" } }, "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api/node_modules/jsonschema": { @@ -7930,9 +7930,9 @@ } }, "node_modules/cdk-from-cfn": { - "version": "0.295.0", - "resolved": "https://registry.npmjs.org/cdk-from-cfn/-/cdk-from-cfn-0.295.0.tgz", - "integrity": "sha512-HNQu3TfNTHZNlxh/o0XxhMMSt3uDFDtMxxO2wZGvZpHwvjZLLFSCHooMbMGj75vtyqNmqKxQdR9WQSTcW3oIpg==", + "version": "0.297.0", + "resolved": "https://registry.npmjs.org/cdk-from-cfn/-/cdk-from-cfn-0.297.0.tgz", + "integrity": "sha512-ZyiugKPe9QYmfXNwbjBhc8sgbos7E0mQfo9P3/vrID8iKTsf5YZswPDu526sBTg4Fpx94N08UJZjLepRxaK4Ng==", "license": "MIT OR Apache-2.0" }, "node_modules/chai": { From ad482cfabe1b2e1591cb4e73e2eec8fae6685676 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:06:35 -0400 Subject: [PATCH 07/45] chore(deps): bump @opentelemetry/resources from 2.6.1 to 2.7.0 (#1026) Bumps [@opentelemetry/resources](https://github.com/open-telemetry/opentelemetry-js) from 2.6.1 to 2.7.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/v2.6.1...v2.7.0) --- updated-dependencies: - dependency-name: "@opentelemetry/resources" dependency-version: 2.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 71 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 45a727fa7..c3d3e2931 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4204,6 +4204,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/resources": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", + "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-logs": { "version": "0.214.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", @@ -4288,6 +4304,22 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/resources": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", + "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/sdk-logs": { "version": "0.214.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.214.0.tgz", @@ -4392,12 +4424,12 @@ } }, "node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.7.0.tgz", + "integrity": "sha512-K+oi0hNMv94EpZbnW3eyu2X6SGVpD3O5DhG2NIp65Hc7lhAj9brRXTAVzh3wB82+q3ThakEf7Zd7RsFUqcTc7A==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", + "@opentelemetry/core": "2.7.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { @@ -4407,6 +4439,21 @@ "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, + "node_modules/@opentelemetry/resources/node_modules/@opentelemetry/core": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.0.tgz", + "integrity": "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-logs": { "version": "0.213.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.213.0.tgz", @@ -4472,6 +4519,22 @@ "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", + "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.3.0 <1.10.0" + } + }, "node_modules/@opentelemetry/sdk-trace-base": { "version": "2.6.0", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.0.tgz", From 56a6d4c133e21bcef944dea880a8398dd9331bf4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:07:04 -0400 Subject: [PATCH 08/45] chore(deps-dev): bump @secretlint/secretlint-rule-preset-recommend (#1028) Bumps [@secretlint/secretlint-rule-preset-recommend](https://github.com/secretlint/secretlint) from 12.2.0 to 12.3.1. - [Release notes](https://github.com/secretlint/secretlint/releases) - [Commits](https://github.com/secretlint/secretlint/compare/v12.2.0...v12.3.1) --- updated-dependencies: - dependency-name: "@secretlint/secretlint-rule-preset-recommend" dependency-version: 12.3.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c3d3e2931..dfac7ac22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5092,9 +5092,9 @@ "license": "MIT" }, "node_modules/@secretlint/secretlint-rule-preset-recommend": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-12.2.0.tgz", - "integrity": "sha512-n4qknL6vYRelmyrAyV/Z8I85c6jS6yF/ZxpgcqebjJuECIiel8OT2wIVIq9vk0MwlQN35skaQu0KvfM8uuGeyA==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/secretlint-rule-preset-recommend/-/secretlint-rule-preset-recommend-12.3.1.tgz", + "integrity": "sha512-w9x9rIP1+qhV0k9k15aSIBo+6NrM4npAYIoNs7UmKIxuQSA6b91rhWpdMxgCv2/EQtym+gpuyN7rTSvG5wlYlw==", "dev": true, "license": "MIT", "engines": { From 36755e9e54787f36fcb37f31d4171ca9aa3900b9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:07:27 -0400 Subject: [PATCH 09/45] chore(deps-dev): bump secretlint from 12.2.0 to 12.3.1 (#1029) Bumps [secretlint](https://github.com/secretlint/secretlint) from 12.2.0 to 12.3.1. - [Release notes](https://github.com/secretlint/secretlint/releases) - [Commits](https://github.com/secretlint/secretlint/compare/v12.2.0...v12.3.1) --- updated-dependencies: - dependency-name: secretlint dependency-version: 12.3.1 dependency-type: direct:development update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 138 +++++++++++++++++++++++----------------------- 1 file changed, 69 insertions(+), 69 deletions(-) diff --git a/package-lock.json b/package-lock.json index dfac7ac22..f3c25ba04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4975,28 +4975,28 @@ "license": "MIT" }, "node_modules/@secretlint/config-creator": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-12.2.0.tgz", - "integrity": "sha512-enoydCMrJ8rmrM09qxDBd2XU1V3u9N9CfjRyUbYh3+m74G17u2PCTnlAw5UyeobewCb06d4Dym5t5ybCabATyA==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/config-creator/-/config-creator-12.3.1.tgz", + "integrity": "sha512-CCRvPfrQLt2fPg3eWTIDGXNcVFQd6ZnvQCZ5lzclV9OF7iRqXQ4l5lfGO8NS68tIZx7YvBKhcO8/eVxdqm89HA==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/types": "12.2.0" + "@secretlint/types": "12.3.1" }, "engines": { "node": ">=22.0.0" } }, "node_modules/@secretlint/config-loader": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-12.2.0.tgz", - "integrity": "sha512-f7B9o6YF1jhTtd0ccJywcliCWkP02eYNM4efmua77AuztQTkXLVsw6eECXGAfZ9vh9uPHAK87Km6X4ta5hhtlA==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/config-loader/-/config-loader-12.3.1.tgz", + "integrity": "sha512-PNrxz8tnAU/y5PmfOtKfVb+zEA3I+1iZqP1f9fXvIBtauBKs0h0Y+Cmvj0gG1a34kxaD1aQvFh8qHEhRV05gWg==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/profiler": "12.2.0", - "@secretlint/resolver": "12.2.0", - "@secretlint/types": "12.2.0", + "@secretlint/profiler": "12.3.1", + "@secretlint/resolver": "12.3.1", + "@secretlint/types": "12.3.1", "ajv": "^8.18.0", "debug": "^4.4.3", "rc-config-loader": "^4.1.4" @@ -5006,14 +5006,14 @@ } }, "node_modules/@secretlint/core": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-12.2.0.tgz", - "integrity": "sha512-ZT4irO8fPUg2810kcnfNQZ+AHIIYLFKyEqR91aSDl3g/RFOOLC66CAzGmMA1OuMc+sx9XE9TnM/IpLmLVvUSnA==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/core/-/core-12.3.1.tgz", + "integrity": "sha512-ulcfARo1TANr8tWzDO/5cFxSNEEfRzgW6YPHYUijgpH3iYfwtUhWEU/r/BiFGl2PNaGzzVE1N9A6nZ74xbYvUQ==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/profiler": "12.2.0", - "@secretlint/types": "12.2.0", + "@secretlint/profiler": "12.3.1", + "@secretlint/types": "12.3.1", "debug": "^4.4.3", "structured-source": "^4.0.0" }, @@ -5022,14 +5022,14 @@ } }, "node_modules/@secretlint/formatter": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-12.2.0.tgz", - "integrity": "sha512-1KDSx4NgKObi8OQoPjBaGa41/sv9ZIrEMa94kQ3PhhVTPONP4N618W2c1CBVMuSNvRHilsKjXWZlKJKcIF4FlQ==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/formatter/-/formatter-12.3.1.tgz", + "integrity": "sha512-SXpTiRzuuFNbHa59zk0eUxFOB/LYxnuHSfqq7zU9lIt0z5rox6NrnN9WWkoQai2V9s7n3VqUVUpZqnhxQ2Jzpw==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/resolver": "12.2.0", - "@secretlint/types": "12.2.0", + "@secretlint/resolver": "12.3.1", + "@secretlint/types": "12.3.1", "@textlint/linter-formatter": "^15.5.4", "@textlint/module-interop": "^15.5.4", "@textlint/types": "^15.5.4", @@ -5058,18 +5058,18 @@ } }, "node_modules/@secretlint/node": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-12.2.0.tgz", - "integrity": "sha512-7hZxi49l2pkGjCT/BQf+ElKqFcbxooH9JslCThRfAMElyL3KGo14HhGfFFyWhhTLH2enAds1nKXczhWBI3otIQ==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/node/-/node-12.3.1.tgz", + "integrity": "sha512-1T08nqwWIJqSRrfkebk4Op5MwYgNnB6gwjv9v+X+V+HEIeG1GB/EgH8CJa8jK4uYdhUuaKyXpu36FIbjNa1wqA==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/config-loader": "12.2.0", - "@secretlint/core": "12.2.0", - "@secretlint/formatter": "12.2.0", - "@secretlint/profiler": "12.2.0", - "@secretlint/source-creator": "12.2.0", - "@secretlint/types": "12.2.0", + "@secretlint/config-loader": "12.3.1", + "@secretlint/core": "12.3.1", + "@secretlint/formatter": "12.3.1", + "@secretlint/profiler": "12.3.1", + "@secretlint/source-creator": "12.3.1", + "@secretlint/types": "12.3.1", "debug": "^4.4.3", "p-map": "^7.0.4" }, @@ -5078,16 +5078,16 @@ } }, "node_modules/@secretlint/profiler": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-12.2.0.tgz", - "integrity": "sha512-tB1NhUbCWH+32wSx6xE+Uj7nTUkidYEyW6B6pdGxsiZSM4SGz+FuKpr9OcylGsEphkkz1cQA3P9CjwCHcQqrnw==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/profiler/-/profiler-12.3.1.tgz", + "integrity": "sha512-lztyqJPTfkY0Ze9P7vNs3zm7p2Wq1+4ilFXVrxin0sDyFVXpkt+0+vsKsmdx9yBHabxrLDZgxa7fIsfV721cLw==", "dev": true, "license": "MIT" }, "node_modules/@secretlint/resolver": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-12.2.0.tgz", - "integrity": "sha512-k3Mq4zeLpJtvBoEggEYstWhEiD23tL8qHbz/eYN+yQaQ2tItebIMd34qFX1jjeooiZdp/OuNWZA5JeyTw+SXcQ==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/resolver/-/resolver-12.3.1.tgz", + "integrity": "sha512-/QwcX5azKRdz9mBIbTBUsqp+cmWQZYGNdOHLbsMOBTLXa7KoEBffhmeaMSc0kNSrdgbgfu/7j+qeeaF4QwJf3A==", "dev": true, "license": "MIT" }, @@ -5102,13 +5102,13 @@ } }, "node_modules/@secretlint/source-creator": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-12.2.0.tgz", - "integrity": "sha512-FYPtOmnm5daQnY4m2mgf/06bXkCL2oj1CIs+76tBu80kE1RDH0/ejsVKsiw6O5H3E2J1ruchRpSXTAlyQw1rYA==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/source-creator/-/source-creator-12.3.1.tgz", + "integrity": "sha512-RCkmyKdoe6VFWMzzVm5a9W+a+ptJSusVX+YOrcNy/heklMIWLg0bL+HYFcyYCm8rU2dRq2HuSYTOamDjNs0LZg==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/types": "12.2.0", + "@secretlint/types": "12.3.1", "istextorbinary": "^9.5.0" }, "engines": { @@ -5116,9 +5116,9 @@ } }, "node_modules/@secretlint/types": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-12.2.0.tgz", - "integrity": "sha512-pIqhdWTFMN/cBfpZkAX1A8dqavsFvAdLobbxyMUHBUn/sUgXzyvUp7I52iyTr21EPc/BvOT9lDWdJBkcNz+n7Q==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/@secretlint/types/-/types-12.3.1.tgz", + "integrity": "sha512-Qv3fKvPkzUJpS9Ps6m2EPjC0RdxS2ZZrRfZAhIdl2u0zSjgf+Z0+AaCngmHRR+3Vtbw6s2FrCf4T6mLirm8Hgg==", "dev": true, "license": "MIT", "engines": { @@ -5865,24 +5865,24 @@ "license": "MIT" }, "node_modules/@textlint/ast-node-types": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.5.4.tgz", - "integrity": "sha512-bVtB6VEy9U9DpW8cTt25k5T+lz86zV5w6ImePZqY1AXzSuPhqQNT77lkMPxonXzUducEIlSvUu3o7sKw3y9+Sw==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/ast-node-types/-/ast-node-types-15.6.0.tgz", + "integrity": "sha512-CxZHFbYAU7J0A4izz31wV2ZZfySR6aVj2OSR6/3tppZm7VV6hM7nA7sutsLwIiBL/v4lsB1RM79l4Dc/VrH4qw==", "dev": true, "license": "MIT" }, "node_modules/@textlint/linter-formatter": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.5.4.tgz", - "integrity": "sha512-D9qJedKBLmAo+kiudop4UKgSxXMi4O8U86KrCidVXZ9RsK0NSVIw6+r2rlMUOExq79iEY81FRENyzmNVRxDBsg==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/linter-formatter/-/linter-formatter-15.6.0.tgz", + "integrity": "sha512-IwHRhjwxs0a5t1eNAoKAdV224CDca38LyopPofXpwO/d0J75wBvzf/cBHXNl4TMsLKhYGtR83UprcLEKj/gZsA==", "dev": true, "license": "MIT", "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", - "@textlint/module-interop": "15.5.4", - "@textlint/resolver": "15.5.4", - "@textlint/types": "15.5.4", + "@textlint/module-interop": "15.6.0", + "@textlint/resolver": "15.6.0", + "@textlint/types": "15.6.0", "chalk": "^4.1.2", "debug": "^4.4.3", "js-yaml": "^4.1.1", @@ -5925,27 +5925,27 @@ } }, "node_modules/@textlint/module-interop": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.5.4.tgz", - "integrity": "sha512-JyAUd26ll3IFF87LP0uGoa8Tzw5ZKiYvGs6v8jLlzyND1lUYCI4+2oIAslrODLkf0qwoCaJrBQWM3wsw+asVGQ==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/module-interop/-/module-interop-15.6.0.tgz", + "integrity": "sha512-MHY6pJx9i5kOlrvUSK51887tYZjHAV2qnr6unBm7LtBLGDFo93utdYqHyWep8r9QLsilQdeijWtufJI46z4v4w==", "dev": true, "license": "MIT" }, "node_modules/@textlint/resolver": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.5.4.tgz", - "integrity": "sha512-5GUagtpQuYcmhlOzBGdmVBvDu5lKgVTjwbxtdfoidN4OIqblIxThJHHjazU+ic+/bCIIzI2JcOjHGSaRmE8Gcg==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/resolver/-/resolver-15.6.0.tgz", + "integrity": "sha512-T1l2Gd3455pwtm0cTewhX/LLy3bL9z6/Fu/am+jj+jjGfXVoknYkjfkZEKrjHlA7xzay0EfUKnu//teYemLeZw==", "dev": true, "license": "MIT" }, "node_modules/@textlint/types": { - "version": "15.5.4", - "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.5.4.tgz", - "integrity": "sha512-mY28j2U7nrWmZbxyKnRvB8vJxJab4AxqOobLfb6iozrLelJbqxcOTvBQednadWPfAk9XWaZVMqUr9Nird3mutg==", + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/@textlint/types/-/types-15.6.0.tgz", + "integrity": "sha512-CvgYb1PiqF4BGyoZebGWzAJCZ4ChJAZ9gtWjpQIMKE4Xe2KlSwDA8m8MsiZIV321f5Ibx38BMjC1Z/2ZYP2GQg==", "dev": true, "license": "MIT", "dependencies": { - "@textlint/ast-node-types": "15.5.4" + "@textlint/ast-node-types": "15.6.0" } }, "node_modules/@trivago/prettier-plugin-sort-imports": { @@ -14528,17 +14528,17 @@ "license": "MIT" }, "node_modules/secretlint": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-12.2.0.tgz", - "integrity": "sha512-nIl6JNhywewJIJGHNeCpu0/NXs4zyhTriz9683SWNIjH6etDyN/Q/L2fJ4nCxqdl7iZM3MlVtQQMtPDomQINuw==", + "version": "12.3.1", + "resolved": "https://registry.npmjs.org/secretlint/-/secretlint-12.3.1.tgz", + "integrity": "sha512-wv8TKCjU5hbBxo5jKEX8wIE78VAoL0Ux7pu18+TxtbICMZ2OCbu6EmO3OJLbUbyfUXSPVryNLNmGVgvwY6Z0xw==", "dev": true, "license": "MIT", "dependencies": { - "@secretlint/config-creator": "12.2.0", - "@secretlint/formatter": "12.2.0", - "@secretlint/node": "12.2.0", - "@secretlint/profiler": "12.2.0", - "@secretlint/resolver": "12.2.0", + "@secretlint/config-creator": "12.3.1", + "@secretlint/formatter": "12.3.1", + "@secretlint/node": "12.3.1", + "@secretlint/profiler": "12.3.1", + "@secretlint/resolver": "12.3.1", "debug": "^4.4.3", "globby": "^16.2.0", "read-pkg": "^10.1.0" From ad59fc05e04e7a745a009eff05e95538398cc8a7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:13:10 -0400 Subject: [PATCH 10/45] chore(deps): bump @opentelemetry/sdk-metrics from 2.6.1 to 2.7.0 (#1030) Bumps [@opentelemetry/sdk-metrics](https://github.com/open-telemetry/opentelemetry-js) from 2.6.1 to 2.7.0. - [Release notes](https://github.com/open-telemetry/opentelemetry-js/releases) - [Changelog](https://github.com/open-telemetry/opentelemetry-js/blob/main/CHANGELOG.md) - [Commits](https://github.com/open-telemetry/opentelemetry-js/compare/v2.6.1...v2.7.0) --- updated-dependencies: - dependency-name: "@opentelemetry/sdk-metrics" dependency-version: 2.7.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 53 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/package-lock.json b/package-lock.json index f3c25ba04..8ad83cceb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4238,6 +4238,22 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.1.tgz", + "integrity": "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, "node_modules/@opentelemetry/exporter-metrics-otlp-http/node_modules/@opentelemetry/sdk-trace-base": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", @@ -4338,6 +4354,22 @@ "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, + "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/sdk-metrics": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.1.tgz", + "integrity": "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==", + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/core": "2.6.1", + "@opentelemetry/resources": "2.6.1" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.9.0 <1.10.0" + } + }, "node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/sdk-trace-base": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.6.1.tgz", @@ -4504,13 +4536,13 @@ } }, "node_modules/@opentelemetry/sdk-metrics": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.6.1.tgz", - "integrity": "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.7.0.tgz", + "integrity": "sha512-Vd7h95av/LYRsAVN7wbprvvJnHkq7swMXAo7Uad0Uxf9jl6NSReLa0JNivrcc5BVIx/vl2t+cgdVQQbnVhsR9w==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", - "@opentelemetry/resources": "2.6.1" + "@opentelemetry/core": "2.7.0", + "@opentelemetry/resources": "2.7.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" @@ -4519,20 +4551,19 @@ "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, - "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/resources": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.6.1.tgz", - "integrity": "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA==", + "node_modules/@opentelemetry/sdk-metrics/node_modules/@opentelemetry/core": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.7.0.tgz", + "integrity": "sha512-DT12SXVwV2eoJrGf4nnsvZojxxeQo+LlNAsoYGRRObPWTeN6APiqZ2+nqDCQDvQX40eLi1AePONS0onoASp3yQ==", "license": "Apache-2.0", "dependencies": { - "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "engines": { "node": "^18.19.0 || >=20.6.0" }, "peerDependencies": { - "@opentelemetry/api": ">=1.3.0 <1.10.0" + "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "node_modules/@opentelemetry/sdk-trace-base": { From d3b412f4be22e2da350e6872ce93f31abd97f8ec Mon Sep 17 00:00:00 2001 From: Aidan Daly <99039782+aidandaly24@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:19:16 -0400 Subject: [PATCH 11/45] fix(ci): update snapshots after CDK version sync in release workflow (#1033) The release workflow syncs @aws/agentcore-cdk to the latest npm version in the asset template, but never updates the snapshot file. This causes the asset snapshot test to fail because the snapshot still holds the old version string. --- .github/workflows/release-main-and-preview.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release-main-and-preview.yml b/.github/workflows/release-main-and-preview.yml index 6f40bf756..7f0790fc6 100644 --- a/.github/workflows/release-main-and-preview.yml +++ b/.github/workflows/release-main-and-preview.yml @@ -120,6 +120,7 @@ jobs: fs.writeFileSync('$TEMPLATE_PKG', JSON.stringify(pkg, null, 2) + '\n'); " echo "✅ Updated @aws/agentcore-cdk: $CURRENT_CDK -> $LATEST_CDK" + npm run test:update-snapshots fi fi @@ -218,6 +219,7 @@ jobs: fs.writeFileSync('$TEMPLATE_PKG', JSON.stringify(pkg, null, 2) + '\n'); " echo "✅ Updated @aws/agentcore-cdk: $CURRENT_CDK -> $LATEST_CDK" + npm run test:update-snapshots fi fi From 061b6b319f27c72e8b8e35206c0561aeb3309497 Mon Sep 17 00:00:00 2001 From: Tejas Kashinath <42380254+tejaskash@users.noreply.github.com> Date: Wed, 29 Apr 2026 14:25:40 -0400 Subject: [PATCH 12/45] fix(ci): enable coverage collection in sharded unit test runs (#1034) The coverage report on PRs was empty (0/0 Unknown%) because the sharded unit-test jobs ran without --coverage. Without that flag, V8 coverage data is never collected, so the blob reports contain no coverage maps. The merge-reports step then merges undefined entries and produces empty results. Also fixes the coverage report action referencing a nonexistent vitest.unit.config.ts (should be vitest.config.ts). --- .github/workflows/build-and-test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 5dc0f86e6..80b17f987 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -87,7 +87,7 @@ jobs: - run: npm ci - run: npm run build --if-present - name: Run unit tests (shard ${{ matrix.shard }}) - run: npx vitest run --project unit --shard=${{ matrix.shard }} --reporter=blob --reporter=verbose + run: npx vitest run --project unit --shard=${{ matrix.shard }} --reporter=blob --reporter=verbose --coverage - name: Upload blob report if: always() uses: actions/upload-artifact@v7 @@ -144,6 +144,6 @@ jobs: with: json-summary-path: coverage/coverage-summary.json json-final-path: coverage/coverage-final.json - vite-config-path: vitest.unit.config.ts + vite-config-path: vitest.config.ts file-coverage-mode: none coverage-thresholds: '{ "lines": 50, "branches": 50, "functions": 50, "statements": 50 }' From 227c8402e1b01cd2fb1fd82037ff04247e921e44 Mon Sep 17 00:00:00 2001 From: Aidan Daly <99039782+aidandaly24@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:02:01 -0400 Subject: [PATCH 13/45] fix(ci): move snapshot update after build in release workflow (#1036) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move `npm run test:update-snapshots` from inside the CDK sync step (before build) to its own step after `npm run build`. The test suite needs built artifacts to pass — running it before the build caused 18 test failures. --- .github/workflows/release-main-and-preview.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-main-and-preview.yml b/.github/workflows/release-main-and-preview.yml index 7f0790fc6..2866ca09a 100644 --- a/.github/workflows/release-main-and-preview.yml +++ b/.github/workflows/release-main-and-preview.yml @@ -120,7 +120,6 @@ jobs: fs.writeFileSync('$TEMPLATE_PKG', JSON.stringify(pkg, null, 2) + '\n'); " echo "✅ Updated @aws/agentcore-cdk: $CURRENT_CDK -> $LATEST_CDK" - npm run test:update-snapshots fi fi @@ -147,6 +146,9 @@ jobs: node scripts/generate-schema.mjs npx prettier --write schemas/ + - name: Update snapshots + run: npm run test:update-snapshots + - name: Create release branch and PR env: GH_TOKEN: ${{ github.token }} @@ -219,7 +221,6 @@ jobs: fs.writeFileSync('$TEMPLATE_PKG', JSON.stringify(pkg, null, 2) + '\n'); " echo "✅ Updated @aws/agentcore-cdk: $CURRENT_CDK -> $LATEST_CDK" - npm run test:update-snapshots fi fi @@ -245,6 +246,9 @@ jobs: node scripts/generate-schema.mjs npx prettier --write schemas/ + - name: Update snapshots + run: npm run test:update-snapshots + - name: Create release branch and PR env: GH_TOKEN: ${{ github.token }} From 29ae8e513d23ea68d07239906a025b824038f4c6 Mon Sep 17 00:00:00 2001 From: Aidan Daly <99039782+aidandaly24@users.noreply.github.com> Date: Wed, 29 Apr 2026 15:25:25 -0400 Subject: [PATCH 14/45] fix(ci): install uv in release workflow prepare steps (#1038) The create.test.ts tests require uv for Python project scaffolding. The Build and Test workflow installs it via astral-sh/setup-uv, but the release workflow's prepare steps were missing it, causing test failures during the snapshot update step. --- .github/workflows/release-main-and-preview.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/release-main-and-preview.yml b/.github/workflows/release-main-and-preview.yml index 2866ca09a..9c15750b8 100644 --- a/.github/workflows/release-main-and-preview.yml +++ b/.github/workflows/release-main-and-preview.yml @@ -99,6 +99,9 @@ jobs: with: node-version: 20.x + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Configure git run: | git config --global user.name "github-actions[bot]" @@ -198,6 +201,9 @@ jobs: with: node-version: 20.x + - name: Install uv + uses: astral-sh/setup-uv@v7 + - name: Configure git run: | git config --global user.name "github-actions[bot]" From c0972326d579f027434f4d50d41054c50df1c483 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 29 Apr 2026 19:35:10 +0000 Subject: [PATCH 15/45] chore: bump version to 0.12.1 --- CHANGELOG.md | 23 +++++++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- .../assets.snapshot.test.ts.snap | 2 +- src/assets/cdk/package.json | 2 +- 5 files changed, 28 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f434ffef..517433820 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,29 @@ All notable changes to this project will be documented in this file. +## [0.12.1] - 2026-04-29 + +### Added +- feat: add CloudWatch traces API for web UI (#997) (76b07aa) + +### Fixed +- fix: remove CONFIG_DIR exclusion from zip stage to preserve dependency agentcore/ packages (#1015) (d1e5241) + +### Other Changes +- fix(ci): install uv in release workflow prepare steps (#1038) (29ae8e5) +- fix(ci): move snapshot update after build in release workflow (#1036) (227c840) +- fix(ci): enable coverage collection in sharded unit test runs (#1034) (061b6b3) +- fix(ci): update snapshots after CDK version sync in release workflow (#1033) (d3b412f) +- chore(deps): bump @opentelemetry/sdk-metrics from 2.6.1 to 2.7.0 (#1030) (ad59fc0) +- chore(deps-dev): bump secretlint from 12.2.0 to 12.3.1 (#1029) (36755e9) +- chore(deps-dev): bump @secretlint/secretlint-rule-preset-recommend (#1028) (56a6d4c) +- chore(deps): bump @opentelemetry/resources from 2.6.1 to 2.7.0 (#1026) (ad482cf) +- chore(deps): bump the aws-cdk group with 2 updates (#1025) (1686e4d) +- chore(deps): bump the aws-sdk group with 14 updates (#1024) (1fc366c) +- ci: add coordinated main + preview release workflow (#995) (7e8cae4) +- fix(import): use GatewayNameSchema for gateway import name validation (#1011) (29b6522) +- test: remove 44 render-only and framework-testing tests (#998) (13b34a3) + ## [0.12.0] - 2026-04-28 ### Added diff --git a/package-lock.json b/package-lock.json index 8ad83cceb..c7bd8c53c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aws/agentcore", - "version": "0.12.0", + "version": "0.12.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aws/agentcore", - "version": "0.12.0", + "version": "0.12.1", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 7b0687a99..326eca3a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aws/agentcore", - "version": "0.12.0", + "version": "0.12.1", "description": "CLI for Amazon Bedrock AgentCore", "license": "Apache-2.0", "repository": { diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index 8fa17f318..e1a1dd106 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -357,7 +357,7 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/package.json should m "typescript": "~5.9.3" }, "dependencies": { - "@aws/agentcore-cdk": "^0.1.0-alpha.19", + "@aws/agentcore-cdk": "0.1.0-alpha.23", "aws-cdk-lib": "^2.248.0", "constructs": "^10.0.0" } diff --git a/src/assets/cdk/package.json b/src/assets/cdk/package.json index aa58892c2..056efec20 100644 --- a/src/assets/cdk/package.json +++ b/src/assets/cdk/package.json @@ -23,7 +23,7 @@ "typescript": "~5.9.3" }, "dependencies": { - "@aws/agentcore-cdk": "^0.1.0-alpha.19", + "@aws/agentcore-cdk": "0.1.0-alpha.23", "aws-cdk-lib": "^2.248.0", "constructs": "^10.0.0" } From 27ce126c82aaab1c61fe76eabbe3892e86cc35f1 Mon Sep 17 00:00:00 2001 From: Aidan Daly <99039782+aidandaly24@users.noreply.github.com> Date: Wed, 29 Apr 2026 16:07:44 -0400 Subject: [PATCH 16/45] fix: remove CDK version auto-sync from release workflow and restore caret range (#1044) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove the auto-sync step that bumped @aws/agentcore-cdk during releases — version updates will be managed manually via PRs. Restore the caret range (^0.1.0-alpha.19) in the asset and snapshot that was dropped by the auto-sync in #1042. --- .../workflows/release-main-and-preview.yml | 36 ------------------- .../assets.snapshot.test.ts.snap | 2 +- src/assets/cdk/package.json | 2 +- 3 files changed, 2 insertions(+), 38 deletions(-) diff --git a/.github/workflows/release-main-and-preview.yml b/.github/workflows/release-main-and-preview.yml index 9c15750b8..b3cfb5ee2 100644 --- a/.github/workflows/release-main-and-preview.yml +++ b/.github/workflows/release-main-and-preview.yml @@ -109,23 +109,6 @@ jobs: - run: npm ci - - name: Sync @aws/agentcore-cdk to latest npm version - run: | - LATEST_CDK=$(npm view @aws/agentcore-cdk version 2>/dev/null || echo "") - if [ -n "$LATEST_CDK" ]; then - TEMPLATE_PKG="src/assets/cdk/package.json" - CURRENT_CDK=$(node -p "require('./$TEMPLATE_PKG').dependencies['@aws/agentcore-cdk']") - if [ "$CURRENT_CDK" != "$LATEST_CDK" ]; then - node -e " - const fs = require('fs'); - const pkg = JSON.parse(fs.readFileSync('$TEMPLATE_PKG', 'utf8')); - pkg.dependencies['@aws/agentcore-cdk'] = '$LATEST_CDK'; - fs.writeFileSync('$TEMPLATE_PKG', JSON.stringify(pkg, null, 2) + '\n'); - " - echo "✅ Updated @aws/agentcore-cdk: $CURRENT_CDK -> $LATEST_CDK" - fi - fi - - name: Bump version id: bump env: @@ -211,25 +194,6 @@ jobs: - run: npm ci - # TODO: When @aws/agentcore-cdk publishes a @preview dist-tag, change this - # to: npm view @aws/agentcore-cdk@preview version - - name: Sync @aws/agentcore-cdk to latest npm version - run: | - LATEST_CDK=$(npm view @aws/agentcore-cdk version 2>/dev/null || echo "") - if [ -n "$LATEST_CDK" ]; then - TEMPLATE_PKG="src/assets/cdk/package.json" - CURRENT_CDK=$(node -p "require('./$TEMPLATE_PKG').dependencies['@aws/agentcore-cdk']") - if [ "$CURRENT_CDK" != "$LATEST_CDK" ]; then - node -e " - const fs = require('fs'); - const pkg = JSON.parse(fs.readFileSync('$TEMPLATE_PKG', 'utf8')); - pkg.dependencies['@aws/agentcore-cdk'] = '$LATEST_CDK'; - fs.writeFileSync('$TEMPLATE_PKG', JSON.stringify(pkg, null, 2) + '\n'); - " - echo "✅ Updated @aws/agentcore-cdk: $CURRENT_CDK -> $LATEST_CDK" - fi - fi - - name: Bump version id: bump env: diff --git a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap index e1a1dd106..8fa17f318 100644 --- a/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap +++ b/src/assets/__tests__/__snapshots__/assets.snapshot.test.ts.snap @@ -357,7 +357,7 @@ exports[`Assets Directory Snapshots > CDK assets > cdk/cdk/package.json should m "typescript": "~5.9.3" }, "dependencies": { - "@aws/agentcore-cdk": "0.1.0-alpha.23", + "@aws/agentcore-cdk": "^0.1.0-alpha.19", "aws-cdk-lib": "^2.248.0", "constructs": "^10.0.0" } diff --git a/src/assets/cdk/package.json b/src/assets/cdk/package.json index 056efec20..aa58892c2 100644 --- a/src/assets/cdk/package.json +++ b/src/assets/cdk/package.json @@ -23,7 +23,7 @@ "typescript": "~5.9.3" }, "dependencies": { - "@aws/agentcore-cdk": "0.1.0-alpha.23", + "@aws/agentcore-cdk": "^0.1.0-alpha.19", "aws-cdk-lib": "^2.248.0", "constructs": "^10.0.0" } From 821e4c394d8760a4fe29a487dbb89c092df6f261 Mon Sep 17 00:00:00 2001 From: Imran Ismail Date: Fri, 1 May 2026 00:33:21 +1200 Subject: [PATCH 17/45] fix: add Accept header to HTTP protocol invocation proxy (#1051) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dev web UI proxy for HTTP protocol agents doesn't include an Accept header when forwarding requests to /invocations. The bedrock-agentcore runtime SDK checks for Accept: text/event-stream before enabling SSE streaming (via @fastify/sse reply.sse), so streaming handlers always get a 406 response in the inspector. A2A and AGUI protocol paths already send the header correctly — this brings HTTP in line with them. Using "text/event-stream, */*" so streaming agents get SSE enabled while non-streaming agents can respond in whatever format they need. --- src/cli/operations/dev/web-ui/handlers/invocations.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli/operations/dev/web-ui/handlers/invocations.ts b/src/cli/operations/dev/web-ui/handlers/invocations.ts index 4123a696d..3a6b70ed9 100644 --- a/src/cli/operations/dev/web-ui/handlers/invocations.ts +++ b/src/cli/operations/dev/web-ui/handlers/invocations.ts @@ -68,6 +68,7 @@ export async function handleInvocations( return new Promise((resolve, reject) => { const headers: Record = { 'Content-Type': 'application/json', + Accept: 'text/event-stream, */*', 'x-amzn-bedrock-agentcore-runtime-session-id': sessionId ?? randomUUID(), }; if (userId) { From 397c18785d2f55b906c32519b12f6f00dca6983b Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Thu, 30 Apr 2026 09:27:36 -0400 Subject: [PATCH 18/45] feat: add telemetry audit mode with FileSystemSink (#1014) * feat: add FilesystemSink for telemetry audit mode * feat: instrument help.modes with telemetry, add audit integ test --- integ-tests/help.test.ts | 63 +++++++++++- src/cli/cli.ts | 8 +- src/cli/commands/help/command.tsx | 19 +++- .../__tests__/filesystem-sink.test.ts | 95 +++++++++++++++++++ src/cli/telemetry/client-accessor.ts | 49 ++++++++++ src/cli/telemetry/config.ts | 5 + src/cli/telemetry/index.ts | 4 +- src/cli/telemetry/schemas/command-run.ts | 1 + src/cli/telemetry/sinks/filesystem-sink.ts | 48 ++++++++++ 9 files changed, 284 insertions(+), 8 deletions(-) create mode 100644 src/cli/telemetry/__tests__/filesystem-sink.test.ts create mode 100644 src/cli/telemetry/client-accessor.ts create mode 100644 src/cli/telemetry/sinks/filesystem-sink.ts diff --git a/integ-tests/help.test.ts b/integ-tests/help.test.ts index af99b8ce4..052605c7a 100644 --- a/integ-tests/help.test.ts +++ b/integ-tests/help.test.ts @@ -1,5 +1,10 @@ +import { spawnAndCollect } from '../src/test-utils/cli-runner.js'; import { runCLI } from '../src/test-utils/index.js'; -import { describe, expect, it } from 'vitest'; +import { readdirSync } from 'node:fs'; +import { mkdir, readFile, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; const COMMANDS = [ 'create', @@ -38,3 +43,59 @@ describe('CLI help', () => { } }); }); + +describe('help modes telemetry', () => { + let testConfigDir: string; + const cliPath = join(__dirname, '..', 'dist', 'cli', 'index.mjs'); + + beforeAll(async () => { + testConfigDir = join(tmpdir(), `agentcore-help-telemetry-${Date.now()}`); + await mkdir(testConfigDir, { recursive: true }); + }); + afterAll(() => rm(testConfigDir, { recursive: true, force: true })); + + function run(args: string[], extraEnv: Record = {}) { + return spawnAndCollect('node', [cliPath, ...args], tmpdir(), { + AGENTCORE_SKIP_INSTALL: '1', + AGENTCORE_CONFIG_DIR: testConfigDir, + ...extraEnv, + }); + } + + it('writes JSONL audit file when audit is enabled via env var', async () => { + const result = await run(['help', 'modes'], { AGENTCORE_TELEMETRY_AUDIT: '1' }); + expect(result.exitCode).toBe(0); + + const telemetryDir = join(testConfigDir, 'telemetry'); + const files = readdirSync(telemetryDir).filter(f => f.startsWith('help-')); + expect(files).toHaveLength(1); + + const content = await readFile(join(telemetryDir, files[0]!), 'utf-8'); + const entry = JSON.parse(content.trim()); + expect(entry.attrs).toMatchObject({ + 'service.name': 'agentcore-cli', + 'agentcore-cli.mode': 'cli', + command_group: 'help', + command: 'help.modes', + exit_reason: 'success', + }); + expect(entry.attrs['agentcore-cli.session_id']).toBeDefined(); + expect(entry.attrs['os.type']).toBeDefined(); + expect(entry.value).toBeGreaterThanOrEqual(0); + }); + + it('does not write audit file when audit is not enabled', async () => { + const telemetryDir = join(testConfigDir, 'telemetry'); + await rm(telemetryDir, { recursive: true, force: true }); + + const result = await run(['help', 'modes']); + expect(result.exitCode).toBe(0); + + try { + const files = readdirSync(telemetryDir); + expect(files).toHaveLength(0); + } catch { + // telemetry dir doesn't exist — correct + } + }); +}); diff --git a/src/cli/cli.ts b/src/cli/cli.ts index 72cf5c50d..b8100c0d8 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -21,6 +21,7 @@ import { registerValidate } from './commands/validate'; import { PACKAGE_VERSION } from './constants'; import { getOrCreateInstallationId } from './global-config'; import { ALL_PRIMITIVES } from './primitives'; +import { TelemetryClientAccessor } from './telemetry'; import { App } from './tui/App'; import { LayoutProvider } from './tui/context'; import { COMMAND_DESCRIPTIONS } from './tui/copy'; @@ -222,7 +223,12 @@ export const main = async (argv: string[]) => { printTelemetryNotice(); } - await program.parseAsync(argv); + TelemetryClientAccessor.init(args[0] ?? 'unknown'); + try { + await program.parseAsync(argv); + } finally { + await TelemetryClientAccessor.shutdown(); + } // Telemetry notice already printed above; only run update check here. await printPostCommandNotices(false, updateCheck); diff --git a/src/cli/commands/help/command.tsx b/src/cli/commands/help/command.tsx index 48d771854..338684d7e 100644 --- a/src/cli/commands/help/command.tsx +++ b/src/cli/commands/help/command.tsx @@ -1,3 +1,4 @@ +import { TelemetryClientAccessor } from '../../telemetry/client-accessor.js'; import type { Command } from '@commander-js/extra-typings'; const MODES_HELP = ` @@ -41,15 +42,23 @@ export const registerHelp = (program: Command) => { const helpCmd = program .command('help') .description('Display help topics') - .action(() => { - console.log('Available help topics: modes'); - console.log('Run `agentcore help ` for details.'); + .action(async () => { + const client = await TelemetryClientAccessor.get(); + await client.withCommandRun('help', () => { + console.log('Available help topics: modes'); + console.log('Run `agentcore help ` for details.'); + return {}; + }); }); helpCmd .command('modes') .description('Explain interactive vs non-interactive modes') - .action(() => { - console.log(MODES_HELP); + .action(async () => { + const client = await TelemetryClientAccessor.get(); + await client.withCommandRun('help.modes', () => { + console.log(MODES_HELP); + return {}; + }); }); }; diff --git a/src/cli/telemetry/__tests__/filesystem-sink.test.ts b/src/cli/telemetry/__tests__/filesystem-sink.test.ts new file mode 100644 index 000000000..50d8d4620 --- /dev/null +++ b/src/cli/telemetry/__tests__/filesystem-sink.test.ts @@ -0,0 +1,95 @@ +import { createTempConfig } from '../../__tests__/helpers/temp-config'; +import { resolveAuditFilePath } from '../config'; +import { FileSystemSink } from '../sinks/filesystem-sink'; +import { readFile } from 'fs/promises'; +import { join } from 'node:path'; +import { afterAll, beforeEach, describe, expect, it } from 'vitest'; + +const tmp = createTempConfig('fs-sink'); +const outputDir = join(tmp.configDir, 'telemetry'); + +function createSink(opts: { dir?: string; log?: (msg: string) => void } = {}) { + const filePath = join(opts.dir ?? outputDir, 'test-session.json'); + return new FileSystemSink({ filePath, log: opts.log }); +} + +function readJsonl(path: string): Promise { + return readFile(path, 'utf-8').then(data => + data + .trim() + .split('\n') + .map(line => JSON.parse(line)) + ); +} + +describe('FileSystemSink', () => { + beforeEach(() => tmp.setup()); + afterAll(() => tmp.cleanup()); + + it('writes each record as a JSONL line on disk', async () => { + const sink = createSink(); + sink.record(42, { command_group: 'deploy', command: 'deploy', exit_reason: 'success' }); + await sink.flush(); + + const entries = await readJsonl(join(outputDir, 'test-session.json')); + expect(entries).toHaveLength(1); + expect(entries[0]).toMatchObject({ + value: 42, + attrs: { command_group: 'deploy', command: 'deploy', exit_reason: 'success' }, + }); + }); + + it('appends multiple records as separate lines', async () => { + const sink = createSink(); + sink.record(10, { command_group: 'add', command: 'add.agent' }); + sink.record(20, { command_group: 'add', command: 'add.memory' }); + await sink.flush(); + + const entries = await readJsonl(join(outputDir, 'test-session.json')); + expect(entries).toHaveLength(2); + expect(entries[0]).toMatchObject({ value: 10 }); + expect(entries[1]).toMatchObject({ value: 20 }); + }); + + it('creates output directory if it does not exist', async () => { + const nested = join(tmp.testDir, 'deep', 'nested', 'telemetry'); + const filePath = join(nested, 'test.json'); + const sink = new FileSystemSink({ filePath }); + sink.record(1, { command_group: 'status', command: 'status' }); + await sink.flush(); + + const entries = await readJsonl(filePath); + expect(entries).toHaveLength(1); + }); + + it('flush is a no-op when no records exist', async () => { + const sink = createSink(); + await expect(sink.flush()).resolves.toBeUndefined(); + }); + + it('shutdown logs audit message when records were written', async () => { + const logged: string[] = []; + const sink = createSink({ log: msg => logged.push(msg) }); + sink.record(99, { command_group: 'invoke', command: 'invoke' }); + await sink.shutdown(); + + expect(logged).toHaveLength(1); + expect(logged[0]).toContain('[audit mode]'); + expect(logged[0]).toContain('test-session.json'); + }); + + it('shutdown does not log when no records were written', async () => { + const logged: string[] = []; + const sink = createSink({ log: msg => logged.push(msg) }); + await sink.shutdown(); + + expect(logged).toHaveLength(0); + }); +}); + +describe('resolveAuditFilePath', () => { + it('joins outputDir, entrypoint, and sessionId into a JSON file path', () => { + const path = resolveAuditFilePath('/home/user/.agentcore/telemetry', 'deploy', 'abc-123'); + expect(path).toBe('/home/user/.agentcore/telemetry/deploy-abc-123.json'); + }); +}); diff --git a/src/cli/telemetry/client-accessor.ts b/src/cli/telemetry/client-accessor.ts new file mode 100644 index 000000000..c41c261df --- /dev/null +++ b/src/cli/telemetry/client-accessor.ts @@ -0,0 +1,49 @@ +import { GLOBAL_CONFIG_DIR, readGlobalConfig } from '../global-config.js'; +import { TelemetryClient } from './client.js'; +import { resolveAuditFilePath, resolveResourceAttributes } from './config.js'; +import { FileSystemSink } from './sinks/filesystem-sink.js'; +import { CompositeSink } from './sinks/metric-sink.js'; +import { join } from 'path'; + +/** + * Manages a singleton TelemetryClient. Call init() at startup to configure, + * get() from command handlers to obtain the client, and shutdown() on exit. + * get() lazily initializes if init() was never called. + */ +export class TelemetryClientAccessor { + private static clientPromise: Promise | undefined; + + static init(entrypoint: string, mode: 'cli' | 'tui' = 'cli'): void { + this.clientPromise = createClient(entrypoint, mode); + } + + static get(): Promise { + this.clientPromise ??= createClient('unknown'); + return this.clientPromise; + } + + static async shutdown(): Promise { + if (this.clientPromise) { + const client = await this.clientPromise; + await client.shutdown(); + } + } +} + +async function createClient(entrypoint: string, mode: 'cli' | 'tui' = 'cli'): Promise { + const [resource, config] = await Promise.all([resolveResourceAttributes(mode), readGlobalConfig()]); + + const sinks = []; + const audit = process.env.AGENTCORE_TELEMETRY_AUDIT === '1' || config.telemetry?.audit === true; + + if (audit) { + const filePath = resolveAuditFilePath( + join(GLOBAL_CONFIG_DIR, 'telemetry'), + entrypoint, + resource['agentcore-cli.session_id'] + ); + sinks.push(new FileSystemSink({ filePath, resource })); + } + + return new TelemetryClient(new CompositeSink(sinks)); +} diff --git a/src/cli/telemetry/config.ts b/src/cli/telemetry/config.ts index 5bee94eff..364d57f68 100644 --- a/src/cli/telemetry/config.ts +++ b/src/cli/telemetry/config.ts @@ -3,6 +3,7 @@ import { getOrCreateInstallationId, readGlobalConfig } from '../global-config.js import { type ResourceAttributes, ResourceAttributesSchema } from './schemas/common-attributes.js'; import { randomUUID } from 'crypto'; import os from 'os'; +import { join } from 'path'; // --------------------------------------------------------------------------- // Telemetry preference (opt-in / opt-out) @@ -59,3 +60,7 @@ export async function resolveResourceAttributes(mode: 'cli' | 'tui'): Promise; + log?: (message: string) => void; +} + +export class FileSystemSink implements MetricSink { + private readonly filePath: string; + private readonly resource: Record; + private readonly log: (message: string) => void; + private hasRecords = false; + + constructor(config: FileSystemSinkConfig) { + this.filePath = config.filePath; + this.resource = config.resource ?? {}; + this.log = config.log ?? (msg => console.log(msg)); + } + + record(value: number, attrs: Record): void { + this.hasRecords = true; + this.pendingWrite = this.pendingWrite.then(() => + this.appendEntry({ value, attrs: { ...this.resource, ...attrs } }) + ); + } + + async flush(): Promise { + await this.pendingWrite; + } + + async shutdown(): Promise { + await this.pendingWrite; + if (this.hasRecords) { + this.log(`[audit mode] Telemetry written to ${this.filePath}`); + } + } + + // Promise chain that serializes async writes so record() can stay synchronous. + private pendingWrite: Promise = Promise.resolve(); + + private async appendEntry(entry: { value: number; attrs: Record }): Promise { + await mkdir(dirname(this.filePath), { recursive: true }); + await appendFile(this.filePath, JSON.stringify(entry) + '\n'); + } +} From aef3890e460a9a06db7f8465a157588bc4b0f7b3 Mon Sep 17 00:00:00 2001 From: Jesse Turner <57651174+jesseturner21@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:00:41 -0400 Subject: [PATCH 19/45] refactor: move harness resources to .github/harness/ (#992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: move harness resources to .github/harness/ Move PR reviewer harness files into a dedicated .github/harness/ directory, separate from the general .github/scripts/ used by Strands workflows. - Move harness_review.py, prompts/ to .github/harness/ - Add Dockerfile for the harness container (dual-token: CLONE_TOKEN for git clones, GITHUB_TOKEN for gh CLI/PR comments) - Add README documenting the harness directory - Update pr-ai-review workflow to reference new path - Update .prettierignore for new prompts location * fix(harness): update Dockerfile comment to accurately describe token handling Tokens are baked into image layers at build time — the previous comment incorrectly implied they were not stored. Updated to make the security posture explicit: the image itself must be treated as a secret. * refactor(harness): use boto3 invoke_harness instead of raw SigV4 HTTP Replace manual SigV4 signing + urllib3 + EventStreamBuffer parsing with the native boto3 bedrock-agentcore client's invoke_harness method. This simplifies the code significantly and leverages the typed event stream response from the SDK. Rejected: keep raw HTTP approach | boto3 now supports invoke_harness natively Confidence: high Scope-risk: narrow Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: Claude Opus 4.6 --- .github/harness/Dockerfile | 33 ++++++ .github/harness/README.md | 39 +++++++ .../python => harness}/harness_review.py | 107 +++++++----------- .../{scripts => harness}/prompts/review.md | 0 .../{scripts => harness}/prompts/system.md | 0 .github/workflows/pr-ai-review.yml | 2 +- .prettierignore | 2 +- 7 files changed, 117 insertions(+), 66 deletions(-) create mode 100644 .github/harness/Dockerfile create mode 100644 .github/harness/README.md rename .github/{scripts/python => harness}/harness_review.py (62%) rename .github/{scripts => harness}/prompts/review.md (100%) rename .github/{scripts => harness}/prompts/system.md (100%) diff --git a/.github/harness/Dockerfile b/.github/harness/Dockerfile new file mode 100644 index 000000000..3deec1a46 --- /dev/null +++ b/.github/harness/Dockerfile @@ -0,0 +1,33 @@ +FROM public.ecr.aws/docker/library/python:3.12-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + curl \ + jq \ + && rm -rf /var/lib/apt/lists/* + +# Install GitHub CLI +RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list \ + && apt-get update \ + && apt-get install -y gh \ + && rm -rf /var/lib/apt/lists/* + +# Tokens are baked into the image at build time. This image must be treated as a +# secret and stored only in a registry with equivalent access controls. +ARG CLONE_TOKEN +ARG GITHUB_TOKEN + +# Configure git to use clone token for HTTPS clones +RUN git config --global url."https://${CLONE_TOKEN}@github.com/".insteadOf "https://github.com/" + +# Persist gh CLI auth so GITHUB_TOKEN doesn't need to be in the environment +RUN mkdir -p /root/.config/gh \ + && echo "github.com:" > /root/.config/gh/hosts.yml \ + && echo " oauth_token: ${GITHUB_TOKEN}" >> /root/.config/gh/hosts.yml \ + && echo " user: agentcore-cli-automation" >> /root/.config/gh/hosts.yml \ + && echo " git_protocol: https" >> /root/.config/gh/hosts.yml + +WORKDIR /opt/workspace diff --git a/.github/harness/README.md b/.github/harness/README.md new file mode 100644 index 000000000..d9ba15c61 --- /dev/null +++ b/.github/harness/README.md @@ -0,0 +1,39 @@ +# Harness Resources + +Container and scripts for AI-powered automation via +[AgentCore Harness](https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html). + +## Structure + +``` +harness/ +├── Dockerfile # Container image for the harness runtime +├── harness_review.py # Invokes the harness to review PRs (SigV4 + event stream) +└── prompts/ + ├── system.md # System prompt (workspace context) + └── review.md # PR review task prompt +``` + +## Current: PR Reviewer + +Reviews pull requests on open/reopen via `.github/workflows/pr-ai-review.yml`. + +### Dual-token setup + +The Dockerfile takes two build args: + +- **`CLONE_TOKEN`** — baked into git config for cloning private repos +- **`GITHUB_TOKEN`** — baked into `gh` CLI auth for posting PR comments + +### Building the container + +```bash +finch build \ + --build-arg CLONE_TOKEN= \ + --build-arg GITHUB_TOKEN= \ + -t pr-reviewer .github/harness/ +``` + +## Future: Tester + +This directory will also house a harness-based test runner. diff --git a/.github/scripts/python/harness_review.py b/.github/harness/harness_review.py similarity index 62% rename from .github/scripts/python/harness_review.py rename to .github/harness/harness_review.py index fbfd0b0f9..455fb6a3d 100644 --- a/.github/scripts/python/harness_review.py +++ b/.github/harness/harness_review.py @@ -1,7 +1,7 @@ """Invoke Bedrock AgentCore Harness to review a GitHub PR. Reads PR_URL from the environment. Streams harness output to stdout. -Uses raw HTTP with SigV4 signing — no custom service model needed. +Uses the boto3 bedrock-agentcore client's invoke_harness API. """ import json @@ -11,11 +11,6 @@ import uuid import boto3 -from botocore.auth import SigV4Auth -from botocore.awsrequest import AWSRequest -from botocore.eventstream import EventStreamBuffer -from urllib.parse import quote -import urllib3 # ANSI color codes CYAN = "\033[36m" @@ -25,7 +20,7 @@ DIM = "\033[2m" RESET = "\033[0m" -SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..") +SCRIPTS_DIR = os.path.dirname(__file__) def read_prompt(filename): @@ -35,50 +30,37 @@ def read_prompt(filename): return f.read() -def invoke_harness(harness_arn, body, region): - """Send a SigV4-signed request to the harness invoke endpoint. Returns a streaming response. - - InvokeHarness is not in standard boto3, so we call the REST API directly. - boto3 is only used to resolve AWS credentials (from env vars, OIDC, etc.) - and sign the request with SigV4. The response is an AWS binary event stream. - """ - session = boto3.Session(region_name=region) - credentials = session.get_credentials().get_frozen_credentials() - url = f"https://bedrock-agentcore.{region}.amazonaws.com/harnesses/invoke?harnessArn={quote(harness_arn, safe='')}" - request = AWSRequest(method="POST", url=url, data=body, headers={ - "Content-Type": "application/json", - "Accept": "application/vnd.amazon.eventstream", - }) - SigV4Auth(credentials, "bedrock-agentcore", region).add_auth(request) - return urllib3.PoolManager().urlopen( - "POST", url, body=body, - headers=dict(request.headers), - preload_content=False, - timeout=urllib3.Timeout(connect=10, read=600), +def invoke_harness_streaming(harness_arn, session_id, system_prompt, messages, model_id, region): + """Call invoke_harness via boto3 and return the event stream.""" + client = boto3.client("bedrock-agentcore", region_name=region) + response = client.invoke_harness( + harnessArn=harness_arn, + runtimeSessionId=session_id, + systemPrompt=[{"text": system_prompt}], + messages=messages, + model={"bedrockModelConfig": {"modelId": model_id}}, ) - - -def parse_events(http_response): - """Yield (event_type, payload) tuples from the harness binary event stream. - - The response arrives as raw bytes in AWS binary event stream format. - EventStreamBuffer reassembles complete events from the 4KB chunks, - and we decode each event's JSON payload before yielding it. - """ - event_buffer = EventStreamBuffer() - for chunk in http_response.stream(4096): - event_buffer.add_data(chunk) - for event in event_buffer: - if event.headers.get(":message-type") == "exception": - payload = json.loads(event.payload.decode("utf-8")) - print(f"\n{RED}ERROR: {payload}{RESET}", file=sys.stderr) - sys.exit(1) - event_type = event.headers.get(":event-type", "") - if event.payload: - yield event_type, json.loads(event.payload.decode("utf-8")) - - -def print_stream(http_response): + return response["stream"] + + +def parse_events(event_stream): + """Yield (event_type, payload) tuples from the boto3 event stream.""" + for event in event_stream: + if "contentBlockStart" in event: + yield "contentBlockStart", event["contentBlockStart"] + elif "contentBlockDelta" in event: + yield "contentBlockDelta", event["contentBlockDelta"] + elif "contentBlockStop" in event: + yield "contentBlockStop", event["contentBlockStop"] + elif "messageStop" in event: + yield "messageStop", event["messageStop"] + elif "internalServerException" in event: + yield "internalServerException", event["internalServerException"] + elif "runtimeClientError" in event: + yield "runtimeClientError", event["runtimeClientError"] + + +def print_stream(event_stream): """Display harness events with GitHub Actions log groups. The harness streams events as the agent works: @@ -112,7 +94,7 @@ def flush_text(): print(f"{DIM}{line}{RESET}", flush=True) text_buffer = "" - for event_type, payload in parse_events(http_response): + for event_type, payload in parse_events(event_stream): if event_type == "contentBlockStart": start = payload.get("start", {}) @@ -171,6 +153,11 @@ def flush_text(): print(f"\n{RED}ERROR: {payload}{RESET}", file=sys.stderr) sys.exit(1) + elif event_type == "runtimeClientError": + close_group() + print(f"\n{RED}ERROR: {payload.get('message', payload)}{RESET}", file=sys.stderr) + sys.exit(1) + close_group() total = time.time() - start_time print(f"\n{GREEN}Review complete.{RESET} {DIM}({iteration} tool calls, {int(total)}s total){RESET}") @@ -200,18 +187,10 @@ def flush_text(): SYSTEM_PROMPT = read_prompt("system.md") REVIEW_PROMPT = read_prompt("review.md").format(pr_url=PR_URL) -request_body = json.dumps({ - "runtimeSessionId": SESSION_ID, - "systemPrompt": [{"text": SYSTEM_PROMPT}], - "messages": [{"role": "user", "content": [{"text": REVIEW_PROMPT}]}], - "model": {"bedrockModelConfig": {"modelId": MODEL_ID}}, -}) - -http_response = invoke_harness(HARNESS_ARN, request_body, REGION) +messages = [{"role": "user", "content": [{"text": REVIEW_PROMPT}]}] -if http_response.status != 200: - error = http_response.read().decode("utf-8") - print(f"{RED}ERROR: HTTP {http_response.status}: {error}{RESET}", file=sys.stderr) - sys.exit(1) +event_stream = invoke_harness_streaming( + HARNESS_ARN, SESSION_ID, SYSTEM_PROMPT, messages, MODEL_ID, REGION +) -print_stream(http_response) +print_stream(event_stream) diff --git a/.github/scripts/prompts/review.md b/.github/harness/prompts/review.md similarity index 100% rename from .github/scripts/prompts/review.md rename to .github/harness/prompts/review.md diff --git a/.github/scripts/prompts/system.md b/.github/harness/prompts/system.md similarity index 100% rename from .github/scripts/prompts/system.md rename to .github/harness/prompts/system.md diff --git a/.github/workflows/pr-ai-review.yml b/.github/workflows/pr-ai-review.yml index 8ababddcc..26878d7a1 100644 --- a/.github/workflows/pr-ai-review.yml +++ b/.github/workflows/pr-ai-review.yml @@ -139,7 +139,7 @@ jobs: env: PR_URL: ${{ steps.pr-url.outputs.url }} HARNESS_ARN: ${{ secrets.HARNESS_ARN }} - run: python .github/scripts/python/harness_review.py + run: python .github/harness/harness_review.py - name: Remove agentcore-harness-reviewing label if: always() diff --git a/.prettierignore b/.prettierignore index 8eda17e39..3b1452b18 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,3 @@ CHANGELOG.md src/assets/**/*.md -.github/scripts/prompts/ +.github/harness/prompts/ From b8a90c90a2b287227e5edd9432912a7fbd07d638 Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Thu, 30 Apr 2026 11:03:30 -0400 Subject: [PATCH 20/45] Revert "refactor: move harness resources to .github/harness/ (#992)" This reverts commit aef3890e460a9a06db7f8465a157588bc4b0f7b3. --- .github/harness/Dockerfile | 33 ------ .github/harness/README.md | 39 ------- .../{harness => scripts}/prompts/review.md | 0 .../{harness => scripts}/prompts/system.md | 0 .../python}/harness_review.py | 107 +++++++++++------- .github/workflows/pr-ai-review.yml | 2 +- .prettierignore | 2 +- 7 files changed, 66 insertions(+), 117 deletions(-) delete mode 100644 .github/harness/Dockerfile delete mode 100644 .github/harness/README.md rename .github/{harness => scripts}/prompts/review.md (100%) rename .github/{harness => scripts}/prompts/system.md (100%) rename .github/{harness => scripts/python}/harness_review.py (62%) diff --git a/.github/harness/Dockerfile b/.github/harness/Dockerfile deleted file mode 100644 index 3deec1a46..000000000 --- a/.github/harness/Dockerfile +++ /dev/null @@ -1,33 +0,0 @@ -FROM public.ecr.aws/docker/library/python:3.12-slim - -# Install system dependencies -RUN apt-get update && apt-get install -y \ - git \ - curl \ - jq \ - && rm -rf /var/lib/apt/lists/* - -# Install GitHub CLI -RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ - > /etc/apt/sources.list.d/github-cli.list \ - && apt-get update \ - && apt-get install -y gh \ - && rm -rf /var/lib/apt/lists/* - -# Tokens are baked into the image at build time. This image must be treated as a -# secret and stored only in a registry with equivalent access controls. -ARG CLONE_TOKEN -ARG GITHUB_TOKEN - -# Configure git to use clone token for HTTPS clones -RUN git config --global url."https://${CLONE_TOKEN}@github.com/".insteadOf "https://github.com/" - -# Persist gh CLI auth so GITHUB_TOKEN doesn't need to be in the environment -RUN mkdir -p /root/.config/gh \ - && echo "github.com:" > /root/.config/gh/hosts.yml \ - && echo " oauth_token: ${GITHUB_TOKEN}" >> /root/.config/gh/hosts.yml \ - && echo " user: agentcore-cli-automation" >> /root/.config/gh/hosts.yml \ - && echo " git_protocol: https" >> /root/.config/gh/hosts.yml - -WORKDIR /opt/workspace diff --git a/.github/harness/README.md b/.github/harness/README.md deleted file mode 100644 index d9ba15c61..000000000 --- a/.github/harness/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Harness Resources - -Container and scripts for AI-powered automation via -[AgentCore Harness](https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html). - -## Structure - -``` -harness/ -├── Dockerfile # Container image for the harness runtime -├── harness_review.py # Invokes the harness to review PRs (SigV4 + event stream) -└── prompts/ - ├── system.md # System prompt (workspace context) - └── review.md # PR review task prompt -``` - -## Current: PR Reviewer - -Reviews pull requests on open/reopen via `.github/workflows/pr-ai-review.yml`. - -### Dual-token setup - -The Dockerfile takes two build args: - -- **`CLONE_TOKEN`** — baked into git config for cloning private repos -- **`GITHUB_TOKEN`** — baked into `gh` CLI auth for posting PR comments - -### Building the container - -```bash -finch build \ - --build-arg CLONE_TOKEN= \ - --build-arg GITHUB_TOKEN= \ - -t pr-reviewer .github/harness/ -``` - -## Future: Tester - -This directory will also house a harness-based test runner. diff --git a/.github/harness/prompts/review.md b/.github/scripts/prompts/review.md similarity index 100% rename from .github/harness/prompts/review.md rename to .github/scripts/prompts/review.md diff --git a/.github/harness/prompts/system.md b/.github/scripts/prompts/system.md similarity index 100% rename from .github/harness/prompts/system.md rename to .github/scripts/prompts/system.md diff --git a/.github/harness/harness_review.py b/.github/scripts/python/harness_review.py similarity index 62% rename from .github/harness/harness_review.py rename to .github/scripts/python/harness_review.py index 455fb6a3d..fbfd0b0f9 100644 --- a/.github/harness/harness_review.py +++ b/.github/scripts/python/harness_review.py @@ -1,7 +1,7 @@ """Invoke Bedrock AgentCore Harness to review a GitHub PR. Reads PR_URL from the environment. Streams harness output to stdout. -Uses the boto3 bedrock-agentcore client's invoke_harness API. +Uses raw HTTP with SigV4 signing — no custom service model needed. """ import json @@ -11,6 +11,11 @@ import uuid import boto3 +from botocore.auth import SigV4Auth +from botocore.awsrequest import AWSRequest +from botocore.eventstream import EventStreamBuffer +from urllib.parse import quote +import urllib3 # ANSI color codes CYAN = "\033[36m" @@ -20,7 +25,7 @@ DIM = "\033[2m" RESET = "\033[0m" -SCRIPTS_DIR = os.path.dirname(__file__) +SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..") def read_prompt(filename): @@ -30,37 +35,50 @@ def read_prompt(filename): return f.read() -def invoke_harness_streaming(harness_arn, session_id, system_prompt, messages, model_id, region): - """Call invoke_harness via boto3 and return the event stream.""" - client = boto3.client("bedrock-agentcore", region_name=region) - response = client.invoke_harness( - harnessArn=harness_arn, - runtimeSessionId=session_id, - systemPrompt=[{"text": system_prompt}], - messages=messages, - model={"bedrockModelConfig": {"modelId": model_id}}, +def invoke_harness(harness_arn, body, region): + """Send a SigV4-signed request to the harness invoke endpoint. Returns a streaming response. + + InvokeHarness is not in standard boto3, so we call the REST API directly. + boto3 is only used to resolve AWS credentials (from env vars, OIDC, etc.) + and sign the request with SigV4. The response is an AWS binary event stream. + """ + session = boto3.Session(region_name=region) + credentials = session.get_credentials().get_frozen_credentials() + url = f"https://bedrock-agentcore.{region}.amazonaws.com/harnesses/invoke?harnessArn={quote(harness_arn, safe='')}" + request = AWSRequest(method="POST", url=url, data=body, headers={ + "Content-Type": "application/json", + "Accept": "application/vnd.amazon.eventstream", + }) + SigV4Auth(credentials, "bedrock-agentcore", region).add_auth(request) + return urllib3.PoolManager().urlopen( + "POST", url, body=body, + headers=dict(request.headers), + preload_content=False, + timeout=urllib3.Timeout(connect=10, read=600), ) - return response["stream"] - - -def parse_events(event_stream): - """Yield (event_type, payload) tuples from the boto3 event stream.""" - for event in event_stream: - if "contentBlockStart" in event: - yield "contentBlockStart", event["contentBlockStart"] - elif "contentBlockDelta" in event: - yield "contentBlockDelta", event["contentBlockDelta"] - elif "contentBlockStop" in event: - yield "contentBlockStop", event["contentBlockStop"] - elif "messageStop" in event: - yield "messageStop", event["messageStop"] - elif "internalServerException" in event: - yield "internalServerException", event["internalServerException"] - elif "runtimeClientError" in event: - yield "runtimeClientError", event["runtimeClientError"] - - -def print_stream(event_stream): + + +def parse_events(http_response): + """Yield (event_type, payload) tuples from the harness binary event stream. + + The response arrives as raw bytes in AWS binary event stream format. + EventStreamBuffer reassembles complete events from the 4KB chunks, + and we decode each event's JSON payload before yielding it. + """ + event_buffer = EventStreamBuffer() + for chunk in http_response.stream(4096): + event_buffer.add_data(chunk) + for event in event_buffer: + if event.headers.get(":message-type") == "exception": + payload = json.loads(event.payload.decode("utf-8")) + print(f"\n{RED}ERROR: {payload}{RESET}", file=sys.stderr) + sys.exit(1) + event_type = event.headers.get(":event-type", "") + if event.payload: + yield event_type, json.loads(event.payload.decode("utf-8")) + + +def print_stream(http_response): """Display harness events with GitHub Actions log groups. The harness streams events as the agent works: @@ -94,7 +112,7 @@ def flush_text(): print(f"{DIM}{line}{RESET}", flush=True) text_buffer = "" - for event_type, payload in parse_events(event_stream): + for event_type, payload in parse_events(http_response): if event_type == "contentBlockStart": start = payload.get("start", {}) @@ -153,11 +171,6 @@ def flush_text(): print(f"\n{RED}ERROR: {payload}{RESET}", file=sys.stderr) sys.exit(1) - elif event_type == "runtimeClientError": - close_group() - print(f"\n{RED}ERROR: {payload.get('message', payload)}{RESET}", file=sys.stderr) - sys.exit(1) - close_group() total = time.time() - start_time print(f"\n{GREEN}Review complete.{RESET} {DIM}({iteration} tool calls, {int(total)}s total){RESET}") @@ -187,10 +200,18 @@ def flush_text(): SYSTEM_PROMPT = read_prompt("system.md") REVIEW_PROMPT = read_prompt("review.md").format(pr_url=PR_URL) -messages = [{"role": "user", "content": [{"text": REVIEW_PROMPT}]}] +request_body = json.dumps({ + "runtimeSessionId": SESSION_ID, + "systemPrompt": [{"text": SYSTEM_PROMPT}], + "messages": [{"role": "user", "content": [{"text": REVIEW_PROMPT}]}], + "model": {"bedrockModelConfig": {"modelId": MODEL_ID}}, +}) + +http_response = invoke_harness(HARNESS_ARN, request_body, REGION) -event_stream = invoke_harness_streaming( - HARNESS_ARN, SESSION_ID, SYSTEM_PROMPT, messages, MODEL_ID, REGION -) +if http_response.status != 200: + error = http_response.read().decode("utf-8") + print(f"{RED}ERROR: HTTP {http_response.status}: {error}{RESET}", file=sys.stderr) + sys.exit(1) -print_stream(event_stream) +print_stream(http_response) diff --git a/.github/workflows/pr-ai-review.yml b/.github/workflows/pr-ai-review.yml index 26878d7a1..8ababddcc 100644 --- a/.github/workflows/pr-ai-review.yml +++ b/.github/workflows/pr-ai-review.yml @@ -139,7 +139,7 @@ jobs: env: PR_URL: ${{ steps.pr-url.outputs.url }} HARNESS_ARN: ${{ secrets.HARNESS_ARN }} - run: python .github/harness/harness_review.py + run: python .github/scripts/python/harness_review.py - name: Remove agentcore-harness-reviewing label if: always() diff --git a/.prettierignore b/.prettierignore index 3b1452b18..8eda17e39 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,3 @@ CHANGELOG.md src/assets/**/*.md -.github/harness/prompts/ +.github/scripts/prompts/ From ad2ba9bcc610ac4dc1ee255e5b5d407e6003c94a Mon Sep 17 00:00:00 2001 From: Jesse Turner Date: Thu, 30 Apr 2026 11:03:33 -0400 Subject: [PATCH 21/45] refactor: move harness resources to .github/harness/ and use boto3 invoke_harness - Move harness_review.py, prompts/ to .github/harness/ - Add Dockerfile for the harness container (dual-token setup) - Add README documenting the harness directory - Update pr-ai-review workflow to reference new path - Replace manual SigV4 signing + urllib3 with native boto3 invoke_harness - Update .prettierignore for new prompts location --- .github/harness/Dockerfile | 33 ++++++ .github/harness/README.md | 39 +++++++ .../python => harness}/harness_review.py | 107 +++++++----------- .../{scripts => harness}/prompts/review.md | 0 .../{scripts => harness}/prompts/system.md | 0 .github/workflows/pr-ai-review.yml | 2 +- .prettierignore | 2 +- 7 files changed, 117 insertions(+), 66 deletions(-) create mode 100644 .github/harness/Dockerfile create mode 100644 .github/harness/README.md rename .github/{scripts/python => harness}/harness_review.py (62%) rename .github/{scripts => harness}/prompts/review.md (100%) rename .github/{scripts => harness}/prompts/system.md (100%) diff --git a/.github/harness/Dockerfile b/.github/harness/Dockerfile new file mode 100644 index 000000000..3deec1a46 --- /dev/null +++ b/.github/harness/Dockerfile @@ -0,0 +1,33 @@ +FROM public.ecr.aws/docker/library/python:3.12-slim + +# Install system dependencies +RUN apt-get update && apt-get install -y \ + git \ + curl \ + jq \ + && rm -rf /var/lib/apt/lists/* + +# Install GitHub CLI +RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /usr/share/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" \ + > /etc/apt/sources.list.d/github-cli.list \ + && apt-get update \ + && apt-get install -y gh \ + && rm -rf /var/lib/apt/lists/* + +# Tokens are baked into the image at build time. This image must be treated as a +# secret and stored only in a registry with equivalent access controls. +ARG CLONE_TOKEN +ARG GITHUB_TOKEN + +# Configure git to use clone token for HTTPS clones +RUN git config --global url."https://${CLONE_TOKEN}@github.com/".insteadOf "https://github.com/" + +# Persist gh CLI auth so GITHUB_TOKEN doesn't need to be in the environment +RUN mkdir -p /root/.config/gh \ + && echo "github.com:" > /root/.config/gh/hosts.yml \ + && echo " oauth_token: ${GITHUB_TOKEN}" >> /root/.config/gh/hosts.yml \ + && echo " user: agentcore-cli-automation" >> /root/.config/gh/hosts.yml \ + && echo " git_protocol: https" >> /root/.config/gh/hosts.yml + +WORKDIR /opt/workspace diff --git a/.github/harness/README.md b/.github/harness/README.md new file mode 100644 index 000000000..d9ba15c61 --- /dev/null +++ b/.github/harness/README.md @@ -0,0 +1,39 @@ +# Harness Resources + +Container and scripts for AI-powered automation via +[AgentCore Harness](https://docs.aws.amazon.com/bedrock/latest/userguide/agentcore.html). + +## Structure + +``` +harness/ +├── Dockerfile # Container image for the harness runtime +├── harness_review.py # Invokes the harness to review PRs (SigV4 + event stream) +└── prompts/ + ├── system.md # System prompt (workspace context) + └── review.md # PR review task prompt +``` + +## Current: PR Reviewer + +Reviews pull requests on open/reopen via `.github/workflows/pr-ai-review.yml`. + +### Dual-token setup + +The Dockerfile takes two build args: + +- **`CLONE_TOKEN`** — baked into git config for cloning private repos +- **`GITHUB_TOKEN`** — baked into `gh` CLI auth for posting PR comments + +### Building the container + +```bash +finch build \ + --build-arg CLONE_TOKEN= \ + --build-arg GITHUB_TOKEN= \ + -t pr-reviewer .github/harness/ +``` + +## Future: Tester + +This directory will also house a harness-based test runner. diff --git a/.github/scripts/python/harness_review.py b/.github/harness/harness_review.py similarity index 62% rename from .github/scripts/python/harness_review.py rename to .github/harness/harness_review.py index fbfd0b0f9..455fb6a3d 100644 --- a/.github/scripts/python/harness_review.py +++ b/.github/harness/harness_review.py @@ -1,7 +1,7 @@ """Invoke Bedrock AgentCore Harness to review a GitHub PR. Reads PR_URL from the environment. Streams harness output to stdout. -Uses raw HTTP with SigV4 signing — no custom service model needed. +Uses the boto3 bedrock-agentcore client's invoke_harness API. """ import json @@ -11,11 +11,6 @@ import uuid import boto3 -from botocore.auth import SigV4Auth -from botocore.awsrequest import AWSRequest -from botocore.eventstream import EventStreamBuffer -from urllib.parse import quote -import urllib3 # ANSI color codes CYAN = "\033[36m" @@ -25,7 +20,7 @@ DIM = "\033[2m" RESET = "\033[0m" -SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..") +SCRIPTS_DIR = os.path.dirname(__file__) def read_prompt(filename): @@ -35,50 +30,37 @@ def read_prompt(filename): return f.read() -def invoke_harness(harness_arn, body, region): - """Send a SigV4-signed request to the harness invoke endpoint. Returns a streaming response. - - InvokeHarness is not in standard boto3, so we call the REST API directly. - boto3 is only used to resolve AWS credentials (from env vars, OIDC, etc.) - and sign the request with SigV4. The response is an AWS binary event stream. - """ - session = boto3.Session(region_name=region) - credentials = session.get_credentials().get_frozen_credentials() - url = f"https://bedrock-agentcore.{region}.amazonaws.com/harnesses/invoke?harnessArn={quote(harness_arn, safe='')}" - request = AWSRequest(method="POST", url=url, data=body, headers={ - "Content-Type": "application/json", - "Accept": "application/vnd.amazon.eventstream", - }) - SigV4Auth(credentials, "bedrock-agentcore", region).add_auth(request) - return urllib3.PoolManager().urlopen( - "POST", url, body=body, - headers=dict(request.headers), - preload_content=False, - timeout=urllib3.Timeout(connect=10, read=600), +def invoke_harness_streaming(harness_arn, session_id, system_prompt, messages, model_id, region): + """Call invoke_harness via boto3 and return the event stream.""" + client = boto3.client("bedrock-agentcore", region_name=region) + response = client.invoke_harness( + harnessArn=harness_arn, + runtimeSessionId=session_id, + systemPrompt=[{"text": system_prompt}], + messages=messages, + model={"bedrockModelConfig": {"modelId": model_id}}, ) - - -def parse_events(http_response): - """Yield (event_type, payload) tuples from the harness binary event stream. - - The response arrives as raw bytes in AWS binary event stream format. - EventStreamBuffer reassembles complete events from the 4KB chunks, - and we decode each event's JSON payload before yielding it. - """ - event_buffer = EventStreamBuffer() - for chunk in http_response.stream(4096): - event_buffer.add_data(chunk) - for event in event_buffer: - if event.headers.get(":message-type") == "exception": - payload = json.loads(event.payload.decode("utf-8")) - print(f"\n{RED}ERROR: {payload}{RESET}", file=sys.stderr) - sys.exit(1) - event_type = event.headers.get(":event-type", "") - if event.payload: - yield event_type, json.loads(event.payload.decode("utf-8")) - - -def print_stream(http_response): + return response["stream"] + + +def parse_events(event_stream): + """Yield (event_type, payload) tuples from the boto3 event stream.""" + for event in event_stream: + if "contentBlockStart" in event: + yield "contentBlockStart", event["contentBlockStart"] + elif "contentBlockDelta" in event: + yield "contentBlockDelta", event["contentBlockDelta"] + elif "contentBlockStop" in event: + yield "contentBlockStop", event["contentBlockStop"] + elif "messageStop" in event: + yield "messageStop", event["messageStop"] + elif "internalServerException" in event: + yield "internalServerException", event["internalServerException"] + elif "runtimeClientError" in event: + yield "runtimeClientError", event["runtimeClientError"] + + +def print_stream(event_stream): """Display harness events with GitHub Actions log groups. The harness streams events as the agent works: @@ -112,7 +94,7 @@ def flush_text(): print(f"{DIM}{line}{RESET}", flush=True) text_buffer = "" - for event_type, payload in parse_events(http_response): + for event_type, payload in parse_events(event_stream): if event_type == "contentBlockStart": start = payload.get("start", {}) @@ -171,6 +153,11 @@ def flush_text(): print(f"\n{RED}ERROR: {payload}{RESET}", file=sys.stderr) sys.exit(1) + elif event_type == "runtimeClientError": + close_group() + print(f"\n{RED}ERROR: {payload.get('message', payload)}{RESET}", file=sys.stderr) + sys.exit(1) + close_group() total = time.time() - start_time print(f"\n{GREEN}Review complete.{RESET} {DIM}({iteration} tool calls, {int(total)}s total){RESET}") @@ -200,18 +187,10 @@ def flush_text(): SYSTEM_PROMPT = read_prompt("system.md") REVIEW_PROMPT = read_prompt("review.md").format(pr_url=PR_URL) -request_body = json.dumps({ - "runtimeSessionId": SESSION_ID, - "systemPrompt": [{"text": SYSTEM_PROMPT}], - "messages": [{"role": "user", "content": [{"text": REVIEW_PROMPT}]}], - "model": {"bedrockModelConfig": {"modelId": MODEL_ID}}, -}) - -http_response = invoke_harness(HARNESS_ARN, request_body, REGION) +messages = [{"role": "user", "content": [{"text": REVIEW_PROMPT}]}] -if http_response.status != 200: - error = http_response.read().decode("utf-8") - print(f"{RED}ERROR: HTTP {http_response.status}: {error}{RESET}", file=sys.stderr) - sys.exit(1) +event_stream = invoke_harness_streaming( + HARNESS_ARN, SESSION_ID, SYSTEM_PROMPT, messages, MODEL_ID, REGION +) -print_stream(http_response) +print_stream(event_stream) diff --git a/.github/scripts/prompts/review.md b/.github/harness/prompts/review.md similarity index 100% rename from .github/scripts/prompts/review.md rename to .github/harness/prompts/review.md diff --git a/.github/scripts/prompts/system.md b/.github/harness/prompts/system.md similarity index 100% rename from .github/scripts/prompts/system.md rename to .github/harness/prompts/system.md diff --git a/.github/workflows/pr-ai-review.yml b/.github/workflows/pr-ai-review.yml index 8ababddcc..26878d7a1 100644 --- a/.github/workflows/pr-ai-review.yml +++ b/.github/workflows/pr-ai-review.yml @@ -139,7 +139,7 @@ jobs: env: PR_URL: ${{ steps.pr-url.outputs.url }} HARNESS_ARN: ${{ secrets.HARNESS_ARN }} - run: python .github/scripts/python/harness_review.py + run: python .github/harness/harness_review.py - name: Remove agentcore-harness-reviewing label if: always() diff --git a/.prettierignore b/.prettierignore index 8eda17e39..3b1452b18 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,3 @@ CHANGELOG.md src/assets/**/*.md -.github/scripts/prompts/ +.github/harness/prompts/ From 278783a98ed3929b0a00b75059ac8bb1c255974e Mon Sep 17 00:00:00 2001 From: Avi Alpert Date: Thu, 30 Apr 2026 11:09:00 -0400 Subject: [PATCH 22/45] feat: update @aws/agent-inspector to 0.3.0 --- browser-tests/tests/traces.test.ts | 30 +++++++++++++++++++++++------- docs/TESTING.md | 18 ++++++++++++++++++ package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 46 insertions(+), 12 deletions(-) diff --git a/browser-tests/tests/traces.test.ts b/browser-tests/tests/traces.test.ts index 77c1b5ad4..0ac481f04 100644 --- a/browser-tests/tests/traces.test.ts +++ b/browser-tests/tests/traces.test.ts @@ -1,22 +1,38 @@ import { expect, sendMessage, test } from '../fixtures'; test.describe('Traces', () => { - test('traces panel shows trace after invocation', async ({ page }) => { + test('traces panel shows span tree after invocation', async ({ page }) => { await page.goto('/'); await sendMessage(page, 'Say hello'); - await page.getByRole('tab', { name: 'Traces' }).click(); + const resourcePanel = page.getByTestId('resource-panel'); + await expect(resourcePanel).toBeVisible({ timeout: 10_000 }); - const traceList = page.getByTestId('trace-list'); + const tracesTab = resourcePanel.getByRole('tab', { name: 'Traces' }); + await tracesTab.click(); + + // Wait for trace list to populate + const traceList = resourcePanel.getByTestId('traces-trace-list'); await expect(traceList).toBeVisible({ timeout: 30_000 }); + // Click the first trace const traceButton = traceList.getByRole('button').first(); - await expect(traceButton).toBeVisible({ timeout: 30_000 }); - + await expect(traceButton).toBeVisible({ timeout: 10_000 }); await traceButton.click(); - const spanRow = page.locator('[role="button"]').filter({ hasText: /.+/ }); - await expect(spanRow.first()).toBeVisible({ timeout: 10_000 }); + // Verify span tree renders + const spanTree = resourcePanel.getByTestId('traces-span-tree'); + await expect(spanTree).toBeVisible({ timeout: 10_000 }); + + // Verify tree has at least one span row with a name + const spanRows = spanTree.getByRole('button'); + await expect(spanRows.first()).toBeVisible(); + + // Click a span to open log panel + await spanRows.first().click(); + + const logPanel = resourcePanel.getByTestId('traces-log-panel'); + await expect(logPanel).toBeVisible({ timeout: 5_000 }); }); }); diff --git a/docs/TESTING.md b/docs/TESTING.md index 700ab3aae..9c70af6b3 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -415,6 +415,24 @@ Test configuration is in `vitest.config.ts` using Vitest projects: - Test timeout: 120 seconds - Hook timeout: 120 seconds +## Troubleshooting + +### `Cannot find module '@playwright/test'` + +Playwright is not installed. Run: + +```bash +npm install +``` + +### `browserType.launch: Executable doesn't exist` (Playwright browsers) + +Playwright browsers need to be downloaded after install. Run: + +```bash +npx playwright install chromium +``` + ## Integration Tests Integration tests require: diff --git a/package-lock.json b/package-lock.json index c7bd8c53c..8adb9bb53 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "@aws-sdk/client-sts": "^3.893.0", "@aws-sdk/client-xray": "^3.1003.0", "@aws-sdk/credential-providers": "^3.893.0", - "@aws/agent-inspector": "0.2.1", + "@aws/agent-inspector": "0.3.0", "@commander-js/extra-typings": "^14.0.0", "@opentelemetry/api": "^1.9.1", "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", @@ -2903,9 +2903,9 @@ } }, "node_modules/@aws/agent-inspector": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@aws/agent-inspector/-/agent-inspector-0.2.1.tgz", - "integrity": "sha512-kyL6RBcTj1hYIchtrHDlDyeqm2viVYMBxhZKVn8wJn058YhI52GIDuUFlKD1avd57X+LJKlHr5VcKvBZp7Sg6A==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/agent-inspector/-/agent-inspector-0.3.0.tgz", + "integrity": "sha512-xD7QPr1WWkT9QWRWo6e9kq8kYxJLQ8egGscgSZ6jCyW3wNV5fcQ6THcAR/71hxxMFF2aleNUc3D8MoqgiS4DVw==", "license": "Apache-2.0", "dependencies": { "@ag-ui/core": "^0.0.52", diff --git a/package.json b/package.json index 326eca3a0..b683a82e0 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@aws-sdk/client-sts": "^3.893.0", "@aws-sdk/client-xray": "^3.1003.0", "@aws-sdk/credential-providers": "^3.893.0", - "@aws/agent-inspector": "0.2.1", + "@aws/agent-inspector": "0.3.0", "@commander-js/extra-typings": "^14.0.0", "@opentelemetry/api": "^1.9.1", "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", From 9a6a5d01138c1b969deacf7dea2b40f0abd56dfb Mon Sep 17 00:00:00 2001 From: Jesse Turner <57651174+jesseturner21@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:27:13 -0400 Subject: [PATCH 23/45] fix(harness): add error handling for invoke_harness API call (#1056) Wrap the invoke_harness_streaming call in a try/except so boto3 errors (bad credentials, network issues, invalid ARN) produce a clean error message instead of a raw traceback in GitHub Actions logs. --- .github/harness/harness_review.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/harness/harness_review.py b/.github/harness/harness_review.py index 455fb6a3d..2ee174266 100644 --- a/.github/harness/harness_review.py +++ b/.github/harness/harness_review.py @@ -189,8 +189,12 @@ def flush_text(): messages = [{"role": "user", "content": [{"text": REVIEW_PROMPT}]}] -event_stream = invoke_harness_streaming( - HARNESS_ARN, SESSION_ID, SYSTEM_PROMPT, messages, MODEL_ID, REGION -) +try: + event_stream = invoke_harness_streaming( + HARNESS_ARN, SESSION_ID, SYSTEM_PROMPT, messages, MODEL_ID, REGION + ) +except Exception as e: + print(f"{RED}ERROR: Failed to invoke harness: {e}{RESET}", file=sys.stderr) + sys.exit(1) print_stream(event_stream) From 5ce4bdc908c1417c4091e883b976ae72a69c0e7c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Thu, 30 Apr 2026 15:59:00 +0000 Subject: [PATCH 24/45] chore: bump version to 0.12.2 Co-authored-by: github-actions[bot] --- CHANGELOG.md | 15 +++++++++++++++ package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 517433820..b25ef328d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to this project will be documented in this file. +## [0.12.2] - 2026-04-30 + +### Added +- feat: add telemetry audit mode with FileSystemSink (#1014) (397c187) + +### Fixed +- fix: add Accept header to HTTP protocol invocation proxy (#1051) (821e4c3) + +### Other Changes +- fix(harness): add error handling for invoke_harness API call (#1056) (9a6a5d0) +- Merge pull request #1054 from aws/fix/remove-coauthor-reland (0afeaf5) +- refactor: move harness resources to .github/harness/ and use boto3 invoke_harness (ad2ba9b) +- Revert "refactor: move harness resources to .github/harness/ (#992)" (b8a90c9) +- refactor: move harness resources to .github/harness/ (#992) (aef3890) + ## [0.12.1] - 2026-04-29 ### Added diff --git a/package-lock.json b/package-lock.json index c7bd8c53c..45ad45ed3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@aws/agentcore", - "version": "0.12.1", + "version": "0.12.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@aws/agentcore", - "version": "0.12.1", + "version": "0.12.2", "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { diff --git a/package.json b/package.json index 326eca3a0..dda0501e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aws/agentcore", - "version": "0.12.1", + "version": "0.12.2", "description": "CLI for Amazon Bedrock AgentCore", "license": "Apache-2.0", "repository": { From 4daca8356837a0d07c0d5d5a70c2fb76b837c6bf Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Thu, 30 Apr 2026 12:28:06 -0400 Subject: [PATCH 25/45] ci: cut full e2e time in half via vitest sharding (#1016) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: shard e2e full suite across 6 runners - Add 6-way vitest sharding to the cdk-source matrix (2 → 12 parallel runners) - Isolate import test resources per run via RESOURCE_SUFFIX to prevent concurrent conflicts * fix: import RESOURCE_SUFFIX in cleanup_resources.py --- .github/workflows/e2e-tests-full.yml | 5 +++-- e2e-tests/fixtures/import/cleanup_resources.py | 10 +++++++--- e2e-tests/fixtures/import/common.py | 6 +++++- e2e-tests/import-gateway.test.ts | 4 +++- e2e-tests/import-resources.test.ts | 4 +++- 5 files changed, 21 insertions(+), 8 deletions(-) diff --git a/.github/workflows/e2e-tests-full.yml b/.github/workflows/e2e-tests-full.yml index 14809a587..e1ff4acab 100644 --- a/.github/workflows/e2e-tests-full.yml +++ b/.github/workflows/e2e-tests-full.yml @@ -27,6 +27,7 @@ jobs: fail-fast: false matrix: cdk-source: [npm, main] + shard: ['1/6', '2/6', '3/6', '4/6', '5/6', '6/6'] steps: - uses: actions/checkout@v6 with: @@ -70,7 +71,7 @@ jobs: CDK_REPO: ${{ secrets.CDK_REPO_NAME }} - name: Install CLI globally run: npm install -g "$(npm pack | tail -1)" - - name: Run E2E tests (${{ matrix.cdk-source }}) + - name: Run E2E tests (${{ matrix.cdk-source }}, shard ${{ matrix.shard }}) env: AWS_ACCOUNT_ID: ${{ steps.aws.outputs.account_id }} AWS_REGION: ${{ inputs.aws_region || 'us-east-1' }} @@ -78,7 +79,7 @@ jobs: OPENAI_API_KEY: ${{ env.E2E_OPENAI_API_KEY }} GEMINI_API_KEY: ${{ env.E2E_GEMINI_API_KEY }} CDK_TARBALL: ${{ env.CDK_TARBALL }} - run: npm run test:e2e + run: npx vitest run --project e2e --shard=${{ matrix.shard }} browser-tests: runs-on: ubuntu-latest environment: e2e-testing diff --git a/e2e-tests/fixtures/import/cleanup_resources.py b/e2e-tests/fixtures/import/cleanup_resources.py index 0728b711e..429a36a1f 100644 --- a/e2e-tests/fixtures/import/cleanup_resources.py +++ b/e2e-tests/fixtures/import/cleanup_resources.py @@ -12,7 +12,7 @@ import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) -from common import REGION, RESOURCES_FILE, get_control_client, get_account_id +from common import REGION, RESOURCE_SUFFIX, RESOURCES_FILE, get_control_client, get_account_id import boto3 @@ -22,8 +22,12 @@ def cleanup_s3_code_objects(): account_id = get_account_id() bucket_name = f"bugbash-agentcore-code-{account_id}-{REGION}" s3 = boto3.client("s3", region_name=REGION) + prefix = f"bugbash-{RESOURCE_SUFFIX}/" if RESOURCE_SUFFIX else "" try: - resp = s3.list_objects_v2(Bucket=bucket_name) + list_args = {"Bucket": bucket_name} + if prefix: + list_args["Prefix"] = prefix + resp = s3.list_objects_v2(**list_args) objects = resp.get("Contents", []) if not objects: return @@ -31,7 +35,7 @@ def cleanup_s3_code_objects(): Bucket=bucket_name, Delete={"Objects": [{"Key": o["Key"]} for o in objects]}, ) - print(f"Deleted {len(objects)} object(s) from s3://{bucket_name}") + print(f"Deleted {len(objects)} object(s) from s3://{bucket_name}/{prefix}") except Exception as e: print(f"Could not clean up S3 objects: {e}") diff --git a/e2e-tests/fixtures/import/common.py b/e2e-tests/fixtures/import/common.py index 369ec0bb0..c786c0c01 100644 --- a/e2e-tests/fixtures/import/common.py +++ b/e2e-tests/fixtures/import/common.py @@ -8,9 +8,11 @@ import boto3 REGION = os.environ.get("AWS_REGION") or os.environ.get("AWS_DEFAULT_REGION") or "us-east-1" +RESOURCE_SUFFIX = os.environ.get("RESOURCE_SUFFIX", "") SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) APP_DIR = os.path.join(SCRIPT_DIR, "app") -RESOURCES_FILE = os.path.join(SCRIPT_DIR, "bugbash-resources.json") +_resources_name = f"bugbash-resources-{RESOURCE_SUFFIX}.json" if RESOURCE_SUFFIX else "bugbash-resources.json" +RESOURCES_FILE = os.path.join(SCRIPT_DIR, _resources_name) INLINE_POLICY_NAME = "bugbash-agentcore-permissions" @@ -35,6 +37,8 @@ def upload_code(prefix="bugbash"): """Zip APP_DIR and upload to S3. Returns (bucket, s3_key).""" bucket_name = get_code_bucket() s3 = boto3.client("s3", region_name=REGION) + if RESOURCE_SUFFIX: + prefix = f"{prefix}-{RESOURCE_SUFFIX}" # Create zip of app directory with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp: diff --git a/e2e-tests/import-gateway.test.ts b/e2e-tests/import-gateway.test.ts index 2aea04f02..fd80ae967 100644 --- a/e2e-tests/import-gateway.test.ts +++ b/e2e-tests/import-gateway.test.ts @@ -43,6 +43,7 @@ describe.sequential('e2e: import gateway', () => { const result = await spawnAndCollect('uv', ['run', '--with', 'boto3', 'python3', 'setup_gateway.py'], fixtureDir, { AWS_REGION: region, + RESOURCE_SUFFIX: suffix, }); if (result.exitCode !== 0) { throw new Error( @@ -50,7 +51,7 @@ describe.sequential('e2e: import gateway', () => { ); } - const resourcesPath = join(fixtureDir, 'bugbash-resources.json'); + const resourcesPath = join(fixtureDir, `bugbash-resources-${suffix}.json`); const resources = JSON.parse(await readFile(resourcesPath, 'utf-8')) as Record; gatewayArn = resources.gateway!.arn; @@ -80,6 +81,7 @@ describe.sequential('e2e: import gateway', () => { try { await spawnAndCollect('uv', ['run', '--with', 'boto3', 'python3', 'cleanup_resources.py'], fixtureDir, { AWS_REGION: region, + RESOURCE_SUFFIX: suffix, }); } catch { /* ignore — resources may already be deleted by CFN teardown */ diff --git a/e2e-tests/import-resources.test.ts b/e2e-tests/import-resources.test.ts index d51cbffac..72d9c253a 100644 --- a/e2e-tests/import-resources.test.ts +++ b/e2e-tests/import-resources.test.ts @@ -54,6 +54,7 @@ describe.sequential('e2e: import runtime/memory/evaluator', () => { const result = await spawnAndCollect('uv', ['run', '--with', 'boto3', 'python3', script], fixtureDir, { AWS_REGION: region, DEFAULT_EVALUATOR_MODEL, + RESOURCE_SUFFIX: suffix, }); if (result.exitCode !== 0) { throw new Error( @@ -63,7 +64,7 @@ describe.sequential('e2e: import runtime/memory/evaluator', () => { } // 2. Read resource ARNs from bugbash-resources.json - const resourcesPath = join(fixtureDir, 'bugbash-resources.json'); + const resourcesPath = join(fixtureDir, `bugbash-resources-${suffix}.json`); const resources = JSON.parse(await readFile(resourcesPath, 'utf-8')) as Record; runtimeArn = resources['runtime-basic']!.arn; memoryArn = resources['memory-full']!.arn; @@ -102,6 +103,7 @@ describe.sequential('e2e: import runtime/memory/evaluator', () => { try { await spawnAndCollect('uv', ['run', '--with', 'boto3', 'python3', 'cleanup_resources.py'], fixtureDir, { AWS_REGION: region, + RESOURCE_SUFFIX: suffix, }); } catch { /* ignore — resources may already be deleted by CFN teardown */ From 3aec000b472f00c122979ff988f0595dd6a30254 Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:59:21 -0400 Subject: [PATCH 26/45] refactor: consolidate cli-config into global-config (#802) --- src/cli/__tests__/global-config.test.ts | 43 ++++++++++++++++++- src/cli/cli.ts | 2 +- .../telemetry/__tests__/telemetry.test.ts | 2 +- src/cli/commands/telemetry/actions.ts | 2 +- .../post-deploy-observability.test.ts | 14 +++--- .../deploy/post-deploy-observability.ts | 4 +- src/cli/telemetry/client-accessor.ts | 2 +- src/cli/telemetry/config.ts | 2 +- src/lib/packaging/build-args.ts | 4 +- src/lib/schemas/io/cli-config.ts | 36 ---------------- src/{cli => lib/schemas/io}/global-config.ts | 29 +++++++++---- src/lib/schemas/io/index.ts | 1 - 12 files changed, 77 insertions(+), 64 deletions(-) delete mode 100644 src/lib/schemas/io/cli-config.ts rename src/{cli => lib/schemas/io}/global-config.ts (73%) diff --git a/src/cli/__tests__/global-config.test.ts b/src/cli/__tests__/global-config.test.ts index 2851a13a4..6e2038973 100644 --- a/src/cli/__tests__/global-config.test.ts +++ b/src/cli/__tests__/global-config.test.ts @@ -1,4 +1,9 @@ -import { getOrCreateInstallationId, readGlobalConfig, updateGlobalConfig } from '../global-config'; +import { + getOrCreateInstallationId, + readGlobalConfig, + readGlobalConfigSync, + updateGlobalConfig, +} from '../../lib/schemas/io/global-config'; import { createTempConfig } from './helpers/temp-config'; import { readFile, writeFile } from 'fs/promises'; import { afterAll, beforeEach, describe, expect, it } from 'vitest'; @@ -21,10 +26,29 @@ describe('global-config', () => { it('returns empty object when file is missing or invalid', async () => { expect(await readGlobalConfig(tmp.testDir + '/nonexistent.json')).toEqual({}); - await writeFile(tmp.configFile, JSON.stringify({ telemetry: { enabled: 'false' } })); + await writeFile(tmp.configFile, 'not json'); expect(await readGlobalConfig(tmp.configFile)).toEqual({}); }); + it('drops invalid fields while preserving valid ones', async () => { + await writeFile( + tmp.configFile, + JSON.stringify({ + transactionSearchIndexPercentage: 'not-a-number', + uvIndex: 'https://valid.url', + telemetry: { enabled: 'yes', endpoint: 'https://example.com' }, + }) + ); + + const config = await readGlobalConfig(tmp.configFile); + + expect(config).toEqual({ + transactionSearchIndexPercentage: undefined, + uvIndex: 'https://valid.url', + telemetry: { enabled: undefined, endpoint: 'https://example.com' }, + }); + }); + it('preserves unknown fields via passthrough', async () => { const full = { installationId: 'abc-123', @@ -39,6 +63,21 @@ describe('global-config', () => { }); }); + describe('readGlobalConfigSync', () => { + it('returns parsed config when file exists', async () => { + await writeFile(tmp.configFile, JSON.stringify({ telemetry: { enabled: false } })); + + expect(readGlobalConfigSync(tmp.configFile)).toEqual({ telemetry: { enabled: false } }); + }); + + it('returns empty object when file is missing or invalid', async () => { + expect(readGlobalConfigSync(tmp.testDir + '/nonexistent.json')).toEqual({}); + + await writeFile(tmp.configFile, 'not json'); + expect(readGlobalConfigSync(tmp.configFile)).toEqual({}); + }); + }); + describe('updateGlobalConfig', () => { it('creates directory and writes config when none exists', async () => { const fresh = createTempConfig('gc-fresh'); diff --git a/src/cli/cli.ts b/src/cli/cli.ts index b8100c0d8..88949ecfe 100644 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -1,3 +1,4 @@ +import { getOrCreateInstallationId } from '../lib/schemas/io/global-config'; import { registerAdd } from './commands/add'; import { registerCreate } from './commands/create'; import { registerDeploy } from './commands/deploy'; @@ -19,7 +20,6 @@ import { registerTraces } from './commands/traces'; import { registerUpdate } from './commands/update'; import { registerValidate } from './commands/validate'; import { PACKAGE_VERSION } from './constants'; -import { getOrCreateInstallationId } from './global-config'; import { ALL_PRIMITIVES } from './primitives'; import { TelemetryClientAccessor } from './telemetry'; import { App } from './tui/App'; diff --git a/src/cli/commands/telemetry/__tests__/telemetry.test.ts b/src/cli/commands/telemetry/__tests__/telemetry.test.ts index b0e615fcd..efdfd2f23 100644 --- a/src/cli/commands/telemetry/__tests__/telemetry.test.ts +++ b/src/cli/commands/telemetry/__tests__/telemetry.test.ts @@ -1,5 +1,5 @@ +import { readGlobalConfig } from '../../../../lib/schemas/io/global-config'; import { createTempConfig } from '../../../__tests__/helpers/temp-config'; -import { readGlobalConfig } from '../../../global-config'; import { handleTelemetryDisable, handleTelemetryEnable, handleTelemetryStatus } from '../actions'; import { chmod, mkdir, rm, writeFile } from 'fs/promises'; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; diff --git a/src/cli/commands/telemetry/actions.ts b/src/cli/commands/telemetry/actions.ts index 90750a0f6..696608e01 100644 --- a/src/cli/commands/telemetry/actions.ts +++ b/src/cli/commands/telemetry/actions.ts @@ -1,4 +1,4 @@ -import { GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_FILE, updateGlobalConfig } from '../../global-config.js'; +import { GLOBAL_CONFIG_DIR, GLOBAL_CONFIG_FILE, updateGlobalConfig } from '../../../lib/schemas/io/global-config.js'; import { resolveTelemetryPreference } from '../../telemetry/config.js'; export async function handleTelemetryDisable( diff --git a/src/cli/operations/deploy/__tests__/post-deploy-observability.test.ts b/src/cli/operations/deploy/__tests__/post-deploy-observability.test.ts index 9155a699d..ba069f29e 100644 --- a/src/cli/operations/deploy/__tests__/post-deploy-observability.test.ts +++ b/src/cli/operations/deploy/__tests__/post-deploy-observability.test.ts @@ -1,23 +1,23 @@ import { setupTransactionSearch } from '../post-deploy-observability.js'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -const { mockEnableTransactionSearch, mockReadCliConfig } = vi.hoisted(() => ({ +const { mockEnableTransactionSearch, mockReadGlobalConfigSync } = vi.hoisted(() => ({ mockEnableTransactionSearch: vi.fn(), - mockReadCliConfig: vi.fn(), + mockReadGlobalConfigSync: vi.fn(), })); vi.mock('../../../aws/transaction-search', () => ({ enableTransactionSearch: mockEnableTransactionSearch, })); -vi.mock('../../../../lib/schemas/io/cli-config', () => ({ - readCliConfig: mockReadCliConfig, +vi.mock('../../../../lib/schemas/io/global-config', () => ({ + readGlobalConfigSync: mockReadGlobalConfigSync, })); describe('setupTransactionSearch', () => { beforeEach(() => { vi.clearAllMocks(); - mockReadCliConfig.mockReturnValue({}); + mockReadGlobalConfigSync.mockReturnValue({}); mockEnableTransactionSearch.mockResolvedValue({ success: true }); }); @@ -33,7 +33,7 @@ describe('setupTransactionSearch', () => { }); it('passes custom transactionSearchIndexPercentage from config', async () => { - mockReadCliConfig.mockReturnValue({ transactionSearchIndexPercentage: 25 }); + mockReadGlobalConfigSync.mockReturnValue({ transactionSearchIndexPercentage: 25 }); const result = await setupTransactionSearch({ region: 'us-east-1', @@ -57,7 +57,7 @@ describe('setupTransactionSearch', () => { }); it('skips when disableTransactionSearch is true in config', async () => { - mockReadCliConfig.mockReturnValue({ disableTransactionSearch: true }); + mockReadGlobalConfigSync.mockReturnValue({ disableTransactionSearch: true }); const result = await setupTransactionSearch({ region: 'us-east-1', diff --git a/src/cli/operations/deploy/post-deploy-observability.ts b/src/cli/operations/deploy/post-deploy-observability.ts index 295392629..0616a65dc 100644 --- a/src/cli/operations/deploy/post-deploy-observability.ts +++ b/src/cli/operations/deploy/post-deploy-observability.ts @@ -1,4 +1,4 @@ -import { readCliConfig } from '../../../lib/schemas/io/cli-config'; +import { readGlobalConfigSync } from '../../../lib/schemas/io/global-config'; import { enableTransactionSearch } from '../../aws/transaction-search'; export interface TransactionSearchSetupOptions { @@ -31,7 +31,7 @@ export async function setupTransactionSearch( return { success: true }; } - const config = readCliConfig(); + const config = readGlobalConfigSync(); if (config.disableTransactionSearch) { return { success: true }; } diff --git a/src/cli/telemetry/client-accessor.ts b/src/cli/telemetry/client-accessor.ts index c41c261df..04172dae5 100644 --- a/src/cli/telemetry/client-accessor.ts +++ b/src/cli/telemetry/client-accessor.ts @@ -1,4 +1,4 @@ -import { GLOBAL_CONFIG_DIR, readGlobalConfig } from '../global-config.js'; +import { GLOBAL_CONFIG_DIR, readGlobalConfig } from '../../lib/schemas/io/global-config.js'; import { TelemetryClient } from './client.js'; import { resolveAuditFilePath, resolveResourceAttributes } from './config.js'; import { FileSystemSink } from './sinks/filesystem-sink.js'; diff --git a/src/cli/telemetry/config.ts b/src/cli/telemetry/config.ts index 364d57f68..fbaa3fb13 100644 --- a/src/cli/telemetry/config.ts +++ b/src/cli/telemetry/config.ts @@ -1,5 +1,5 @@ +import { getOrCreateInstallationId, readGlobalConfig } from '../../lib/schemas/io/global-config.js'; import { PACKAGE_VERSION } from '../constants.js'; -import { getOrCreateInstallationId, readGlobalConfig } from '../global-config.js'; import { type ResourceAttributes, ResourceAttributesSchema } from './schemas/common-attributes.js'; import { randomUUID } from 'crypto'; import os from 'os'; diff --git a/src/lib/packaging/build-args.ts b/src/lib/packaging/build-args.ts index e5af34045..eadc35875 100644 --- a/src/lib/packaging/build-args.ts +++ b/src/lib/packaging/build-args.ts @@ -1,11 +1,11 @@ -import { readCliConfig } from '../schemas/io/cli-config'; +import { readGlobalConfigSync } from '../schemas/io/global-config'; /** * Return Docker --build-arg flags for UV index URLs configured in ~/.agentcore/config.json. * Returns an empty array when no custom indexes are configured. */ export function getUvBuildArgs(): string[] { - const config = readCliConfig(); + const config = readGlobalConfigSync(); const args: string[] = []; if (config.uvDefaultIndex) args.push('--build-arg', `UV_DEFAULT_INDEX=${config.uvDefaultIndex}`); if (config.uvIndex) args.push('--build-arg', `UV_INDEX=${config.uvIndex}`); diff --git a/src/lib/schemas/io/cli-config.ts b/src/lib/schemas/io/cli-config.ts deleted file mode 100644 index aa36d82f1..000000000 --- a/src/lib/schemas/io/cli-config.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { readFileSync } from 'fs'; -import { homedir } from 'os'; -import { join } from 'path'; - -const CONFIG_FILE = join(homedir(), '.agentcore', 'config.json'); - -export interface CliConfig { - uvDefaultIndex?: string; - uvIndex?: string; - disableTransactionSearch?: boolean; - transactionSearchIndexPercentage?: number; -} - -/** - * Read the global CLI config from ~/.agentcore/config.json. - * Returns an empty object if the file doesn't exist or is malformed. - */ -export function readCliConfig(): CliConfig { - try { - const data = readFileSync(CONFIG_FILE, 'utf-8'); - const parsed: Record = JSON.parse(data) as Record; - const config: CliConfig = {}; - if (typeof parsed.uvDefaultIndex === 'string') config.uvDefaultIndex = parsed.uvDefaultIndex; - if (typeof parsed.uvIndex === 'string') config.uvIndex = parsed.uvIndex; - if (parsed.disableTransactionSearch === true) config.disableTransactionSearch = true; - if (typeof parsed.transactionSearchIndexPercentage === 'number') { - const pct = parsed.transactionSearchIndexPercentage; - if (pct >= 0 && pct <= 100) { - config.transactionSearchIndexPercentage = pct; - } - } - return config; - } catch { - return {}; - } -} diff --git a/src/cli/global-config.ts b/src/lib/schemas/io/global-config.ts similarity index 73% rename from src/cli/global-config.ts rename to src/lib/schemas/io/global-config.ts index 267ad6669..fc64eb39b 100644 --- a/src/cli/global-config.ts +++ b/src/lib/schemas/io/global-config.ts @@ -1,3 +1,4 @@ +import { readFileSync } from 'fs'; import { mkdir, readFile, writeFile } from 'fs/promises'; import { randomUUID } from 'node:crypto'; import { homedir } from 'os'; @@ -9,18 +10,19 @@ export const GLOBAL_CONFIG_FILE = join(GLOBAL_CONFIG_DIR, 'config.json'); const GlobalConfigSchema = z .object({ - installationId: z.string().optional(), - uvDefaultIndex: z.string().optional(), - uvIndex: z.string().optional(), - disableTransactionSearch: z.boolean().optional(), - transactionSearchIndexPercentage: z.number().min(0).max(100).optional(), + installationId: z.string().optional().catch(undefined), + uvDefaultIndex: z.string().optional().catch(undefined), + uvIndex: z.string().optional().catch(undefined), + disableTransactionSearch: z.boolean().optional().catch(undefined), + transactionSearchIndexPercentage: z.number().int().min(0).max(100).optional().catch(undefined), telemetry: z .object({ - enabled: z.boolean().optional(), - endpoint: z.string().optional(), - audit: z.boolean().optional(), + enabled: z.boolean().optional().catch(undefined), + endpoint: z.string().optional().catch(undefined), + audit: z.boolean().optional().catch(undefined), }) - .optional(), + .optional() + .catch(undefined), }) .passthrough(); @@ -35,6 +37,15 @@ export async function readGlobalConfig(configFile = GLOBAL_CONFIG_FILE): Promise } } +export function readGlobalConfigSync(configFile = GLOBAL_CONFIG_FILE): GlobalConfig { + try { + const data = readFileSync(configFile, 'utf-8'); + return GlobalConfigSchema.parse(JSON.parse(data)); + } catch { + return {}; + } +} + export async function updateGlobalConfig( partial: GlobalConfig, configDir = GLOBAL_CONFIG_DIR, diff --git a/src/lib/schemas/io/index.ts b/src/lib/schemas/io/index.ts index e8ddddc0f..212468ffe 100644 --- a/src/lib/schemas/io/index.ts +++ b/src/lib/schemas/io/index.ts @@ -11,4 +11,3 @@ export { type PathConfig, } from './path-resolver'; export { ConfigIO, createConfigIO, getSchemaUrlForVersion } from './config-io'; -export { readCliConfig, type CliConfig } from './cli-config'; From a4c37a2a3b92f856a7fd221c033207d386247503 Mon Sep 17 00:00:00 2001 From: Hweinstock <42325418+Hweinstock@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:00:11 -0400 Subject: [PATCH 27/45] feat: make parsing resilient to individual failures (#1062) --- src/cli/telemetry/__tests__/client.test.ts | 37 +++++++++++-- src/cli/telemetry/client.ts | 17 ++++-- .../schemas/__tests__/command-run.test.ts | 54 ++++++++++++++++++- src/cli/telemetry/schemas/common-shapes.ts | 18 +++++++ 4 files changed, 117 insertions(+), 9 deletions(-) diff --git a/src/cli/telemetry/__tests__/client.test.ts b/src/cli/telemetry/__tests__/client.test.ts index e254524bf..96adebafc 100644 --- a/src/cli/telemetry/__tests__/client.test.ts +++ b/src/cli/telemetry/__tests__/client.test.ts @@ -116,18 +116,49 @@ describe('TelemetryClient', () => { expect(sink.metrics[0]!.attrs.check_only).toBe('true'); }); - it('silently drops invalid success payloads', async () => { + it('publishes metric with unknown defaults for incomplete success payloads', async () => { const sink = new InMemorySink(); const client = new TelemetryClient(sink); - // Missing required attrs for 'create' — should silently drop + // Missing required attrs for 'create' — should still publish with 'unknown' defaults await client.withCommandRun( 'create', // @ts-expect-error — intentionally incomplete async () => ({ language: 'python' }) ); - expect(sink.metrics).toHaveLength(0); + expect(sink.metrics).toHaveLength(1); + expect(sink.metrics[0]!.attrs).toMatchObject({ + exit_reason: 'success', + language: 'python', + framework: 'unknown', + model_provider: 'unknown', + }); + }); + + it('defaults invalid attrs to unknown while preserving valid ones', async () => { + const sink = new InMemorySink(); + const client = new TelemetryClient(sink); + + await client.withCommandRun( + 'create', + // @ts-expect-error — intentionally invalid enum value + async () => ({ + language: 'rust', // invalid enum + framework: 'strands', + model_provider: 'bedrock', + memory: 'shortterm', + protocol: 'mcp', + build: 'codezip', + agent_type: 'create', + network_mode: 'public', + has_agent: true, + }) + ); + + expect(sink.metrics).toHaveLength(1); + expect(sink.metrics[0]!.attrs.language).toBe('unknown'); + expect(sink.metrics[0]!.attrs.framework).toBe('strands'); }); it('records cancel when callback returns CANCELLED', async () => { diff --git a/src/cli/telemetry/client.ts b/src/cli/telemetry/client.ts index 3228f45b1..91dffd94f 100644 --- a/src/cli/telemetry/client.ts +++ b/src/cli/telemetry/client.ts @@ -1,6 +1,6 @@ import { classifyError, isUserError } from './error-classification.js'; import { COMMAND_SCHEMAS, type Command, type CommandAttrs, deriveCommandGroup } from './schemas/command-run.js'; -import { type CommandResult, CommandResultSchema } from './schemas/common-shapes.js'; +import { type CommandResult, CommandResultSchema, resilientParse } from './schemas/common-shapes.js'; import type { MetricSink } from './sinks/metric-sink.js'; import { performance } from 'perf_hooks'; @@ -69,17 +69,24 @@ export class TelemetryClient { durationMs: number ): void { try { + // CommandResult is built internally — hard parse is intentional since + // a metric without a valid exit_reason is meaningless. CommandResultSchema.parse(result); - if (result.exit_reason !== 'failure' && result.exit_reason !== 'cancel') { - COMMAND_SCHEMAS[command].parse(attrs); - } + + // Validate command attrs resiliently: invalid fields default to 'unknown' + // instead of dropping the entire metric. + // On failure/cancel the callback attrs are empty so validation is skipped. + const validatedAttrs = + result.exit_reason !== 'failure' && result.exit_reason !== 'cancel' + ? resilientParse(COMMAND_SCHEMAS[command], attrs as Record) + : attrs; const otelAttrs: Record = { command_group: deriveCommandGroup(command), command, }; - for (const obj of [result, attrs]) { + for (const obj of [result, validatedAttrs]) { for (const [k, v] of Object.entries(obj)) { if (typeof v === 'boolean') { otelAttrs[k] = String(v); diff --git a/src/cli/telemetry/schemas/__tests__/command-run.test.ts b/src/cli/telemetry/schemas/__tests__/command-run.test.ts index 11d293c71..110df1284 100644 --- a/src/cli/telemetry/schemas/__tests__/command-run.test.ts +++ b/src/cli/telemetry/schemas/__tests__/command-run.test.ts @@ -1,6 +1,6 @@ import { COMMAND_SCHEMAS, type Command, type CommandAttrs, deriveCommandGroup } from '../command-run'; import { ResourceAttributesSchema } from '../common-attributes'; -import { CommandResultSchema } from '../common-shapes'; +import { CommandResultSchema, resilientParse } from '../common-shapes'; import { describe, expect, expectTypeOf, it } from 'vitest'; import { z } from 'zod'; @@ -170,3 +170,55 @@ describe('type safety', () => { } }); }); + +describe('resilientParse', () => { + it('passes valid attrs through unchanged', () => { + const attrs = { + language: 'python', + framework: 'strands', + model_provider: 'bedrock', + memory: 'shortterm', + protocol: 'mcp', + build: 'codezip', + agent_type: 'create', + network_mode: 'public', + has_agent: true, + }; + expect(resilientParse(COMMAND_SCHEMAS.create, attrs)).toEqual(attrs); + }); + + it('defaults a single invalid enum field to unknown', () => { + const attrs = { + language: 'rust', // invalid + framework: 'strands', + model_provider: 'bedrock', + memory: 'shortterm', + protocol: 'mcp', + build: 'codezip', + agent_type: 'create', + network_mode: 'public', + has_agent: true, + }; + const result = resilientParse(COMMAND_SCHEMAS.create, attrs); + expect(result.language).toBe('unknown'); + expect(result.framework).toBe('strands'); + }); + + it('defaults missing required fields to unknown', () => { + const result = resilientParse(COMMAND_SCHEMAS.create, { language: 'python' }); + expect(result.language).toBe('python'); + expect(result.framework).toBe('unknown'); + expect(result.model_provider).toBe('unknown'); + }); + + it('defaults all fields to unknown when all are invalid', () => { + const result = resilientParse(COMMAND_SCHEMAS.create, {}); + for (const value of Object.values(result)) { + expect(value).toBe('unknown'); + } + }); + + it('returns empty object for no-attrs schemas', () => { + expect(resilientParse(COMMAND_SCHEMAS['telemetry.disable'], {})).toEqual({}); + }); +}); diff --git a/src/cli/telemetry/schemas/common-shapes.ts b/src/cli/telemetry/schemas/common-shapes.ts index 5c5e56493..732bb3d61 100644 --- a/src/cli/telemetry/schemas/common-shapes.ts +++ b/src/cli/telemetry/schemas/common-shapes.ts @@ -8,6 +8,24 @@ export function safeSchema>(shape: T) { return z.object(shape); } +/** + * Validate each field in a schema individually, defaulting to 'unknown' on failure. + * This ensures a single invalid attribute never blocks the entire metric from being published. + * Keys in attrs not present in the schema are omitted from the result. + */ +export function resilientParse( + schema: z.ZodObject, + attrs: Record +): Record { + const result: Record = {}; + for (const key of Object.keys(schema.shape)) { + const field = schema.shape[key] as z.ZodType; + const parsed = field.safeParse(attrs[key]); + result[key] = parsed.success ? parsed.data : 'unknown'; + } + return result; +} + // Primitive types export const Count = z.number().int().nonnegative(); From 2a6caaf5d30b89688f44dd8a30407990fcc06269 Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Wed, 29 Apr 2026 20:35:08 +0000 Subject: [PATCH 28/45] feat: wire telemetry withCommandRun into all add.* commands --- src/cli/primitives/AgentPrimitive.tsx | 157 +++++---- src/cli/primitives/CredentialPrimitive.tsx | 94 +++--- src/cli/primitives/EvaluatorPrimitive.ts | 195 +++++------ src/cli/primitives/GatewayPrimitive.ts | 89 ++--- src/cli/primitives/GatewayTargetPrimitive.ts | 314 ++++++++++-------- src/cli/primitives/MemoryPrimitive.tsx | 79 +++-- .../primitives/OnlineEvalConfigPrimitive.ts | 68 ++-- src/cli/primitives/PolicyEnginePrimitive.ts | 69 ++-- src/cli/primitives/PolicyPrimitive.ts | 71 ++-- .../primitives/RuntimeEndpointPrimitive.ts | 37 ++- src/cli/telemetry/schemas/command-run.ts | 1 + src/cli/telemetry/schemas/common-shapes.ts | 6 + 12 files changed, 658 insertions(+), 522 deletions(-) diff --git a/src/cli/primitives/AgentPrimitive.tsx b/src/cli/primitives/AgentPrimitive.tsx index 4702633ed..77518dc58 100644 --- a/src/cli/primitives/AgentPrimitive.tsx +++ b/src/cli/primitives/AgentPrimitive.tsx @@ -34,6 +34,19 @@ import { import { executeImportAgent } from '../operations/agent/import'; import { setupPythonProject } from '../operations/python'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { + AgentType, + AuthorizerType, + Build, + Framework, + Language, + Memory, + ModelProvider as ModelProviderEnum, + NetworkMode as NetworkModeEnum, + Protocol, + standardize, +} from '../telemetry/schemas/common-shapes.js'; import { createRenderer } from '../templates'; import { requireTTY } from '../tui/guards/tty'; import type { GenerateConfig, MemoryOption } from '../tui/screens/generate/types'; @@ -264,73 +277,93 @@ export class AgentPrimitive extends BasePrimitive { + const validation = validateAddAgentOptions(cliOptions); + if (!validation.valid) { + throw new Error(validation.error); + } + + // Parse custom claims JSON if provided (already validated by validateAddAgentOptions) + const customClaims = cliOptions.customClaims + ? (JSON.parse(cliOptions.customClaims) as CustomClaimValidation[]) + : undefined; + + // Parse request header allowlist if provided + const requestHeaderAllowlist = cliOptions.requestHeaderAllowlist + ? parseAndNormalizeHeaders(cliOptions.requestHeaderAllowlist) + : undefined; + + const result = await this.add({ + name: cliOptions.name!, + type: cliOptions.type ?? 'create', + buildType: (cliOptions.build as BuildType) ?? 'CodeZip', + language: cliOptions.language!, + framework: cliOptions.framework!, + modelProvider: cliOptions.modelProvider!, + apiKey: cliOptions.apiKey, + memory: cliOptions.memory, + protocol: cliOptions.protocol, + networkMode: cliOptions.networkMode, + subnets: cliOptions.subnets, + securityGroups: cliOptions.securityGroups, + requestHeaderAllowlist, + codeLocation: cliOptions.codeLocation, + entrypoint: cliOptions.entrypoint, + bedrockAgentId: cliOptions.agentId, + bedrockAliasId: cliOptions.agentAliasId, + bedrockRegion: cliOptions.region, + authorizerType: cliOptions.authorizerType, + discoveryUrl: cliOptions.discoveryUrl, + allowedAudience: cliOptions.allowedAudience, + allowedClients: cliOptions.allowedClients, + allowedScopes: cliOptions.allowedScopes, + customClaims, + clientId: cliOptions.clientId, + clientSecret: cliOptions.clientSecret, + idleTimeout: cliOptions.idleTimeout ? Number(cliOptions.idleTimeout) : undefined, + maxLifetime: cliOptions.maxLifetime ? Number(cliOptions.maxLifetime) : undefined, + sessionStorageMountPath: cliOptions.sessionStorageMountPath, + }); + + if (!result.success) { + throw new Error(result.error); + } + + if (cliOptions.json) { + console.log(JSON.stringify(result)); + } else { + console.log(`Added agent '${result.agentName}'`); + if (result.agentPath) { + console.log(`Agent code: ${result.agentPath}`); + } + if (cliOptions.networkMode === 'VPC') { + console.log(`\x1b[33mNote: ${VPC_ENDPOINT_WARNING}\x1b[0m`); + } + } + + return { + language: standardize(Language, cliOptions.language!), + framework: standardize(Framework, cliOptions.framework!), + model_provider: standardize(ModelProviderEnum, cliOptions.modelProvider!), + agent_type: standardize(AgentType, cliOptions.type ?? 'create'), + build: standardize(Build, cliOptions.build ?? 'CodeZip'), + protocol: standardize(Protocol, cliOptions.protocol ?? 'HTTP'), + network_mode: standardize(NetworkModeEnum, cliOptions.networkMode ?? 'PUBLIC'), + authorizer_type: standardize(AuthorizerType, cliOptions.authorizerType ?? 'NONE'), + memory: standardize(Memory, cliOptions.memory ?? 'none'), + }; + }); + process.exit(0); + } catch (error) { if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: validation.error })); + console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); } else { - console.error(validation.error); + console.error(getErrorMessage(error)); } process.exit(1); } - - // Parse custom claims JSON if provided (already validated by validateAddAgentOptions) - const customClaims = cliOptions.customClaims - ? (JSON.parse(cliOptions.customClaims) as CustomClaimValidation[]) - : undefined; - - // Parse request header allowlist if provided - const requestHeaderAllowlist = cliOptions.requestHeaderAllowlist - ? parseAndNormalizeHeaders(cliOptions.requestHeaderAllowlist) - : undefined; - - const result = await this.add({ - name: cliOptions.name!, - type: cliOptions.type ?? 'create', - buildType: (cliOptions.build as BuildType) ?? 'CodeZip', - language: cliOptions.language!, - framework: cliOptions.framework!, - modelProvider: cliOptions.modelProvider!, - apiKey: cliOptions.apiKey, - memory: cliOptions.memory, - protocol: cliOptions.protocol, - networkMode: cliOptions.networkMode, - subnets: cliOptions.subnets, - securityGroups: cliOptions.securityGroups, - requestHeaderAllowlist, - codeLocation: cliOptions.codeLocation, - entrypoint: cliOptions.entrypoint, - bedrockAgentId: cliOptions.agentId, - bedrockAliasId: cliOptions.agentAliasId, - bedrockRegion: cliOptions.region, - authorizerType: cliOptions.authorizerType, - discoveryUrl: cliOptions.discoveryUrl, - allowedAudience: cliOptions.allowedAudience, - allowedClients: cliOptions.allowedClients, - allowedScopes: cliOptions.allowedScopes, - customClaims, - clientId: cliOptions.clientId, - clientSecret: cliOptions.clientSecret, - idleTimeout: cliOptions.idleTimeout ? Number(cliOptions.idleTimeout) : undefined, - maxLifetime: cliOptions.maxLifetime ? Number(cliOptions.maxLifetime) : undefined, - sessionStorageMountPath: cliOptions.sessionStorageMountPath, - }); - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else if (result.success) { - console.log(`Added agent '${result.agentName}'`); - if (result.agentPath) { - console.log(`Agent code: ${result.agentPath}`); - } - if (cliOptions.networkMode === 'VPC') { - console.log(`\x1b[33mNote: ${VPC_ENDPOINT_WARNING}\x1b[0m`); - } - } else { - console.error(result.error); - } - - process.exit(result.success ? 0 : 1); } else { // TUI fallback — dynamic imports to avoid pulling ink (async) into registry requireTTY(); diff --git a/src/cli/primitives/CredentialPrimitive.tsx b/src/cli/primitives/CredentialPrimitive.tsx index 52f578235..ba9ddfc2d 100644 --- a/src/cli/primitives/CredentialPrimitive.tsx +++ b/src/cli/primitives/CredentialPrimitive.tsx @@ -4,6 +4,8 @@ import { CredentialSchema } from '../../schema'; import { validateAddCredentialOptions } from '../commands/add/validate'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { CredentialType, standardize } from '../telemetry/schemas/common-shapes.js'; import { requireTTY } from '../tui/guards/tty'; import { BasePrimitive } from './BasePrimitive'; import { computeDefaultCredentialEnvVarName } from './credential-utils'; @@ -290,54 +292,58 @@ export class CredentialPrimitive extends BasePrimitive { + const validation = validateAddCredentialOptions({ + name: cliOptions.name, + type: cliOptions.type as 'api-key' | 'oauth' | undefined, + apiKey: cliOptions.apiKey, + discoveryUrl: cliOptions.discoveryUrl, + clientId: cliOptions.clientId, + clientSecret: cliOptions.clientSecret, + scopes: cliOptions.scopes, + }); + + if (!validation.valid) { + throw new Error(validation.error); + } + + const addOptions = + cliOptions.type === 'oauth' + ? { + authorizerType: 'OAuthCredentialProvider' as const, + name: cliOptions.name!, + discoveryUrl: cliOptions.discoveryUrl!, + clientId: cliOptions.clientId!, + clientSecret: cliOptions.clientSecret!, + scopes: cliOptions.scopes + ?.split(',') + .map(s => s.trim()) + .filter(Boolean), + } + : { + authorizerType: 'ApiKeyCredentialProvider' as const, + name: cliOptions.name!, + apiKey: cliOptions.apiKey!, + }; + + const result = await this.add(addOptions); + + if (!result.success) { + throw new Error(result.error); + } - if (!validation.valid) { if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: validation.error })); + console.log(JSON.stringify(result)); } else { - console.error(validation.error); + console.log(`Added credential '${result.credentialName}'`); } - process.exit(1); - } - - const addOptions = - cliOptions.type === 'oauth' - ? { - authorizerType: 'OAuthCredentialProvider' as const, - name: cliOptions.name!, - discoveryUrl: cliOptions.discoveryUrl!, - clientId: cliOptions.clientId!, - clientSecret: cliOptions.clientSecret!, - scopes: cliOptions.scopes - ?.split(',') - .map(s => s.trim()) - .filter(Boolean), - } - : { - authorizerType: 'ApiKeyCredentialProvider' as const, - name: cliOptions.name!, - apiKey: cliOptions.apiKey!, - }; - - const result = await this.add(addOptions); - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else if (result.success) { - console.log(`Added credential '${result.credentialName}'`); - } else { - console.error(result.error); - } - process.exit(result.success ? 0 : 1); + + return { + credential_type: standardize(CredentialType, cliOptions.type ?? 'api-key'), + }; + }); + process.exit(0); } else { // TUI fallback — dynamic imports to avoid pulling ink (async) into registry requireTTY(); diff --git a/src/cli/primitives/EvaluatorPrimitive.ts b/src/cli/primitives/EvaluatorPrimitive.ts index 73aaf8073..355bd0ce9 100644 --- a/src/cli/primitives/EvaluatorPrimitive.ts +++ b/src/cli/primitives/EvaluatorPrimitive.ts @@ -3,6 +3,8 @@ import type { EvaluationLevel, Evaluator, EvaluatorConfig } from '../../schema'; import { EvaluationLevelSchema, EvaluatorSchema } from '../../schema'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { EvaluatorType, Level, standardize } from '../telemetry/schemas/common-shapes.js'; import { renderCodeBasedEvaluatorTemplate } from '../templates/EvaluatorRenderer'; import { requireTTY } from '../tui/guards/tty'; import { @@ -203,118 +205,123 @@ export class EvaluatorPrimitive extends BasePrimitive { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error })); - } else { - console.error(error); + const client = await TelemetryClientAccessor.get(); + await client.withCommandRun('add.evaluator', async () => { + const fail = (error: string): never => { + throw new Error(error); + }; + + if (!cliOptions.name || !cliOptions.level) { + fail('--name and --level are required in non-interactive mode'); } - process.exit(1); - }; - - if (!cliOptions.name || !cliOptions.level) { - fail('--name and --level are required in non-interactive mode'); - } - - const levelResult = EvaluationLevelSchema.safeParse(cliOptions.level); - if (!levelResult.success) { - fail(`Invalid --level "${cliOptions.level}". Must be one of: SESSION, TRACE, TOOL_CALL`); - } - - const evalType = cliOptions.type ?? 'llm-as-a-judge'; - if (evalType !== 'llm-as-a-judge' && evalType !== 'code-based') { - fail(`Invalid --type "${evalType}". Must be one of: llm-as-a-judge, code-based`); - } - - // Cross-validate flags against evaluator type - if (evalType !== 'code-based') { - if (cliOptions.lambdaArn) fail('--lambda-arn requires --type code-based'); - if (cliOptions.timeout) fail('--timeout requires --type code-based'); - } - if (evalType === 'code-based') { - if (cliOptions.model) fail('--model cannot be used with --type code-based'); - if (cliOptions.instructions) fail('--instructions cannot be used with --type code-based'); - if (cliOptions.ratingScale) fail('--rating-scale cannot be used with --type code-based'); - } - - let configJson: EvaluatorConfig; - - if (cliOptions.config) { - const { readFileSync } = await import('fs'); - configJson = JSON.parse(readFileSync(cliOptions.config, 'utf-8')) as EvaluatorConfig; - } else if (evalType === 'code-based') { - configJson = this.buildCodeBasedConfig(cliOptions.name!, cliOptions.lambdaArn, cliOptions.timeout); - } else { - // LLM-as-a-Judge flow - if (!cliOptions.model) { - fail('Either --config or --model is required for LLM-as-a-Judge evaluators'); + + const levelResult = EvaluationLevelSchema.safeParse(cliOptions.level); + if (!levelResult.success) { + fail(`Invalid --level "${cliOptions.level}". Must be one of: SESSION, TRACE, TOOL_CALL`); } - if (!cliOptions.instructions) { - const level = levelResult.data!; - const placeholders = LEVEL_PLACEHOLDERS[level].map(p => `{${p}}`).join(', '); - fail( - `--instructions is required in non-interactive mode (or use --config). ` + - `Must include at least one placeholder for ${level}: ${placeholders}` - ); + const evalType = cliOptions.type ?? 'llm-as-a-judge'; + if (evalType !== 'llm-as-a-judge' && evalType !== 'code-based') { + fail(`Invalid --type "${evalType}". Must be one of: llm-as-a-judge, code-based`); } - const placeholderCheck = validateInstructionPlaceholders(cliOptions.instructions!, levelResult.data!); - if (placeholderCheck !== true) { - fail(placeholderCheck); + // Cross-validate flags against evaluator type + if (evalType !== 'code-based') { + if (cliOptions.lambdaArn) fail('--lambda-arn requires --type code-based'); + if (cliOptions.timeout) fail('--timeout requires --type code-based'); + } + if (evalType === 'code-based') { + if (cliOptions.model) fail('--model cannot be used with --type code-based'); + if (cliOptions.instructions) fail('--instructions cannot be used with --type code-based'); + if (cliOptions.ratingScale) fail('--rating-scale cannot be used with --type code-based'); } - let ratingScale: NonNullable['ratingScale']; - const scaleInput = cliOptions.ratingScale ?? '1-5-quality'; + let configJson: EvaluatorConfig; - const preset = RATING_SCALE_PRESETS.find(p => p.id === scaleInput); - if (preset) { - ratingScale = preset.ratingScale; + if (cliOptions.config) { + const { readFileSync } = await import('fs'); + configJson = JSON.parse(readFileSync(cliOptions.config, 'utf-8')) as EvaluatorConfig; + } else if (evalType === 'code-based') { + configJson = this.buildCodeBasedConfig(cliOptions.name!, cliOptions.lambdaArn, cliOptions.timeout); } else { - const isNumerical = /^\d/.test(scaleInput.trim()); - const parsed = parseCustomRatingScale(scaleInput, isNumerical ? 'numerical' : 'categorical'); - if (!parsed.success) { + // LLM-as-a-Judge flow + if (!cliOptions.model) { + fail('Either --config or --model is required for LLM-as-a-Judge evaluators'); + } + + if (!cliOptions.instructions) { + const level = levelResult.data!; + const placeholders = LEVEL_PLACEHOLDERS[level].map(p => `{${p}}`).join(', '); fail( - `Invalid --rating-scale "${scaleInput}". Use a preset (${presetIds.join(', ')}) ` + - `or custom format: "1:Label:Definition, 2:Label:Definition" (numerical) ` + - `or "Label:Definition, Label:Definition" (categorical)` + `--instructions is required in non-interactive mode (or use --config). ` + + `Must include at least one placeholder for ${level}: ${placeholders}` ); } - ratingScale = parsed.success ? parsed.ratingScale : undefined!; + + const placeholderCheck = validateInstructionPlaceholders(cliOptions.instructions!, levelResult.data!); + if (placeholderCheck !== true) { + fail(placeholderCheck); + } + + let ratingScale: NonNullable['ratingScale']; + const scaleInput = cliOptions.ratingScale ?? '1-5-quality'; + + const preset = RATING_SCALE_PRESETS.find(p => p.id === scaleInput); + if (preset) { + ratingScale = preset.ratingScale; + } else { + const isNumerical = /^\d/.test(scaleInput.trim()); + const parsed = parseCustomRatingScale(scaleInput, isNumerical ? 'numerical' : 'categorical'); + if (!parsed.success) { + fail( + `Invalid --rating-scale "${scaleInput}". Use a preset (${presetIds.join(', ')}) ` + + `or custom format: "1:Label:Definition, 2:Label:Definition" (numerical) ` + + `or "Label:Definition, Label:Definition" (categorical)` + ); + } + ratingScale = parsed.success ? parsed.ratingScale : undefined!; + } + + configJson = { + llmAsAJudge: { + model: cliOptions.model!, + instructions: cliOptions.instructions!, + ratingScale, + }, + }; } - configJson = { - llmAsAJudge: { - model: cliOptions.model!, - instructions: cliOptions.instructions!, - ratingScale, - }, - }; - } + const result = await this.add({ + name: cliOptions.name!, + level: levelResult.data!, + config: configJson, + }); - const result = await this.add({ - name: cliOptions.name!, - level: levelResult.data!, - config: configJson, - }); + if (!result.success) { + throw new Error(result.error); + } - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else if (result.success) { - if (result.codePath) { - console.log(`Created evaluator '${result.evaluatorName}'`); - console.log(` Code: ${result.codePath}lambda_function.py`); - console.log(` IAM: ${result.codePath}execution-role-policy.json`); - console.log( - `\n Next: Edit lambda_function.py with your evaluation logic, then run \`agentcore deploy\`` - ); + if (cliOptions.json) { + console.log(JSON.stringify(result)); } else { - console.log(`Added evaluator '${result.evaluatorName}'`); + if (result.codePath) { + console.log(`Created evaluator '${result.evaluatorName}'`); + console.log(` Code: ${result.codePath}lambda_function.py`); + console.log(` IAM: ${result.codePath}execution-role-policy.json`); + console.log( + `\n Next: Edit lambda_function.py with your evaluation logic, then run \`agentcore deploy\`` + ); + } else { + console.log(`Added evaluator '${result.evaluatorName}'`); + } } - } else { - console.error(result.error); - } - process.exit(result.success ? 0 : 1); + + return { + evaluator_type: standardize(EvaluatorType, evalType), + level: standardize(Level, levelResult.data!), + }; + }); + process.exit(0); } else { // TUI fallback requireTTY(); diff --git a/src/cli/primitives/GatewayPrimitive.ts b/src/cli/primitives/GatewayPrimitive.ts index 427aa1533..04be5b473 100644 --- a/src/cli/primitives/GatewayPrimitive.ts +++ b/src/cli/primitives/GatewayPrimitive.ts @@ -12,6 +12,8 @@ import type { AddGatewayOptions as CLIAddGatewayOptions } from '../commands/add/ import { validateAddGatewayOptions } from '../commands/add/validate'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { AuthorizerType, PolicyEngineMode, standardize } from '../telemetry/schemas/common-shapes.js'; import { requireTTY } from '../tui/guards/tty'; import type { AddGatewayConfig } from '../tui/screens/mcp/types'; import { BasePrimitive } from './BasePrimitive'; @@ -188,48 +190,61 @@ export class GatewayPrimitive extends BasePrimitive { + const validation = validateAddGatewayOptions(cliOptions); + if (!validation.valid) { + throw new Error(validation.error); + } + + // Parse custom claims JSON if provided (already validated) + const parsedCustomClaims = cliOptions.customClaims + ? (JSON.parse(cliOptions.customClaims) as CustomClaimValidation[]) + : undefined; + + const result = await this.add({ + name: cliOptions.name!, + description: cliOptions.description, + authorizerType: cliOptions.authorizerType ?? 'NONE', + discoveryUrl: cliOptions.discoveryUrl, + allowedAudience: cliOptions.allowedAudience, + allowedClients: cliOptions.allowedClients, + allowedScopes: cliOptions.allowedScopes, + customClaims: parsedCustomClaims, + clientId: cliOptions.clientId, + clientSecret: cliOptions.clientSecret, + runtimes: cliOptions.runtimes, + enableSemanticSearch: cliOptions.semanticSearch !== false, + exceptionLevel: cliOptions.exceptionLevel, + policyEngine: cliOptions.policyEngine, + policyEngineMode: cliOptions.policyEngineMode, + }); + + if (!result.success) { + throw new Error(result.error); + } + if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: validation.error })); + console.log(JSON.stringify(result)); } else { - console.error(validation.error); + console.log(`Added gateway '${result.gatewayName}'`); } - process.exit(1); - } - // Parse custom claims JSON if provided (already validated) - const parsedCustomClaims = cliOptions.customClaims - ? (JSON.parse(cliOptions.customClaims) as CustomClaimValidation[]) - : undefined; - - const result = await this.add({ - name: cliOptions.name!, - description: cliOptions.description, - authorizerType: cliOptions.authorizerType ?? 'NONE', - discoveryUrl: cliOptions.discoveryUrl, - allowedAudience: cliOptions.allowedAudience, - allowedClients: cliOptions.allowedClients, - allowedScopes: cliOptions.allowedScopes, - customClaims: parsedCustomClaims, - clientId: cliOptions.clientId, - clientSecret: cliOptions.clientSecret, - runtimes: cliOptions.runtimes, - enableSemanticSearch: cliOptions.semanticSearch !== false, - exceptionLevel: cliOptions.exceptionLevel, - policyEngine: cliOptions.policyEngine, - policyEngineMode: cliOptions.policyEngineMode, + const runtimeCount = cliOptions.runtimes + ? cliOptions.runtimes + .split(',') + .map(s => s.trim()) + .filter(Boolean).length + : 0; + return { + authorizer_type: standardize(AuthorizerType, cliOptions.authorizerType ?? 'NONE'), + has_policy_engine: !!cliOptions.policyEngine, + policy_engine_mode: standardize(PolicyEngineMode, cliOptions.policyEngineMode ?? 'log_only'), + semantic_search: cliOptions.semanticSearch !== false, + runtime_count: runtimeCount, + }; }); - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else if (result.success) { - console.log(`Added gateway '${result.gatewayName}'`); - } else { - console.error(result.error); - } - - process.exit(result.success ? 0 : 1); + process.exit(0); } catch (error) { if (cliOptions.json) { console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); diff --git a/src/cli/primitives/GatewayTargetPrimitive.ts b/src/cli/primitives/GatewayTargetPrimitive.ts index 41a2e6a75..09c146fbb 100644 --- a/src/cli/primitives/GatewayTargetPrimitive.ts +++ b/src/cli/primitives/GatewayTargetPrimitive.ts @@ -14,6 +14,8 @@ import { validateAddGatewayTargetOptions } from '../commands/add/validate'; import { getErrorMessage } from '../errors'; import type { RemovableGatewayTarget } from '../operations/remove/remove-gateway-target'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { GatewayTargetHost, OutboundAuth, standardize } from '../telemetry/schemas/common-shapes.js'; import { getTemplateToolDefinitions, renderGatewayTargetTemplate } from '../templates/GatewayTargetRenderer'; import { requireTTY } from '../tui/guards/tty'; import type { @@ -303,170 +305,194 @@ export class GatewayTargetPrimitive extends BasePrimitive { + const validation = await validateAddGatewayTargetOptions(cliOptions); + if (!validation.valid) { + throw new Error(validation.error); } - process.exit(1); - } - // Map CLI flag values to internal types - const outboundAuthMap: Record = { - oauth: 'OAUTH', - 'api-key': 'API_KEY', - api_key: 'API_KEY', - none: 'NONE', - }; - - // Handle API Gateway targets (no code generation) - if (cliOptions.type === 'apiGateway') { - const config: ApiGatewayTargetConfig = { - targetType: 'apiGateway', - name: cliOptions.name!, - gateway: cliOptions.gateway!, - restApiId: cliOptions.restApiId!, - stage: cliOptions.stage!, - toolFilters: cliOptions.toolFilterPath - ? [ - { - filterPath: cliOptions.toolFilterPath, - methods: (cliOptions.toolFilterMethods?.split(',').map(m => m.trim()) ?? [ - 'GET', - ]) as ApiGatewayHttpMethod[], - }, - ] - : undefined, - ...(cliOptions.outboundAuthType - ? { - outboundAuth: { - type: (outboundAuthMap[cliOptions.outboundAuthType.toLowerCase()] ?? 'NONE') as - | 'API_KEY' - | 'NONE', - credentialName: cliOptions.credentialName, - }, - } - : {}), + // Map CLI flag values to internal types + const outboundAuthMap: Record = { + oauth: 'OAUTH', + 'api-key': 'API_KEY', + api_key: 'API_KEY', + none: 'NONE', }; - const result = await this.createApiGatewayTarget(config); - const output = { success: true, toolName: result.toolName }; - if (cliOptions.json) { - console.log(JSON.stringify(output)); - } else { - console.log(`Added gateway target '${result.toolName}'`); + + // Map target type from camelCase CLI to kebab-case telemetry + const targetTypeMap = { + apiGateway: 'api-gateway', + openApiSchema: 'open-api-schema', + smithyModel: 'smithy-model', + lambdaFunctionArn: 'lambda-function-arn', + mcpServer: 'mcp-server', + } as const; + type TargetTypeKey = keyof typeof targetTypeMap; + + const cliType = cliOptions.type ?? ''; + const telemetryTargetType = + cliType in targetTypeMap ? targetTypeMap[cliType as TargetTypeKey] : ('mcp-server' as const); + const telemetryOutboundAuth = standardize( + OutboundAuth, + (cliOptions.outboundAuthType ?? 'none').replace('_', '-') + ); + const telemetryHost = standardize(GatewayTargetHost, cliOptions.host ?? 'lambda'); + + // Handle API Gateway targets (no code generation) + if (cliOptions.type === 'apiGateway') { + const config: ApiGatewayTargetConfig = { + targetType: 'apiGateway', + name: cliOptions.name!, + gateway: cliOptions.gateway!, + restApiId: cliOptions.restApiId!, + stage: cliOptions.stage!, + toolFilters: cliOptions.toolFilterPath + ? [ + { + filterPath: cliOptions.toolFilterPath, + methods: (cliOptions.toolFilterMethods?.split(',').map(m => m.trim()) ?? [ + 'GET', + ]) as ApiGatewayHttpMethod[], + }, + ] + : undefined, + ...(cliOptions.outboundAuthType + ? { + outboundAuth: { + type: (outboundAuthMap[cliOptions.outboundAuthType.toLowerCase()] ?? 'NONE') as + | 'API_KEY' + | 'NONE', + credentialName: cliOptions.credentialName, + }, + } + : {}), + }; + const result = await this.createApiGatewayTarget(config); + const output = { success: true, toolName: result.toolName }; + if (cliOptions.json) { + console.log(JSON.stringify(output)); + } else { + console.log(`Added gateway target '${result.toolName}'`); + } + return { target_type: telemetryTargetType, host: telemetryHost, outbound_auth: telemetryOutboundAuth }; } - process.exit(0); - } - // Handle schema-based targets (OpenAPI / Smithy) - if ((cliOptions.type === 'openApiSchema' || cliOptions.type === 'smithyModel') && cliOptions.schema) { - const isS3 = cliOptions.schema.startsWith('s3://'); - const schemaSource = isS3 - ? { - s3: { - uri: cliOptions.schema, - ...(cliOptions.schemaS3Account ? { bucketOwnerAccountId: cliOptions.schemaS3Account } : {}), - }, - } - : { inline: { path: cliOptions.schema } }; - - const config: SchemaBasedTargetConfig = { - name: cliOptions.name!, - targetType: cliOptions.type, - schemaSource, - gateway: cliOptions.gateway!, - ...(cliOptions.outboundAuthType + // Handle schema-based targets (OpenAPI / Smithy) + if ((cliOptions.type === 'openApiSchema' || cliOptions.type === 'smithyModel') && cliOptions.schema) { + const isS3 = cliOptions.schema.startsWith('s3://'); + const schemaSource = isS3 ? { - outboundAuth: { - type: outboundAuthMap[cliOptions.outboundAuthType.toLowerCase()] ?? 'NONE', - credentialName: cliOptions.credentialName, + s3: { + uri: cliOptions.schema, + ...(cliOptions.schemaS3Account ? { bucketOwnerAccountId: cliOptions.schemaS3Account } : {}), }, } - : {}), - }; - const result = await this.createSchemaBasedGatewayTarget(config); - const output = { success: true, toolName: result.toolName }; - if (cliOptions.json) { - console.log(JSON.stringify(output)); - } else { - console.log(`Added gateway target '${result.toolName}'`); + : { inline: { path: cliOptions.schema } }; + + const config: SchemaBasedTargetConfig = { + name: cliOptions.name!, + targetType: cliOptions.type, + schemaSource, + gateway: cliOptions.gateway!, + ...(cliOptions.outboundAuthType + ? { + outboundAuth: { + type: outboundAuthMap[cliOptions.outboundAuthType.toLowerCase()] ?? 'NONE', + credentialName: cliOptions.credentialName, + }, + } + : {}), + }; + const result = await this.createSchemaBasedGatewayTarget(config); + const output = { success: true, toolName: result.toolName }; + if (cliOptions.json) { + console.log(JSON.stringify(output)); + } else { + console.log(`Added gateway target '${result.toolName}'`); + } + return { target_type: telemetryTargetType, host: telemetryHost, outbound_auth: telemetryOutboundAuth }; } - process.exit(0); - } - // Handle Lambda Function ARN targets (no code generation) - if (cliOptions.type === 'lambdaFunctionArn') { - const config = { - targetType: 'lambdaFunctionArn' as const, - name: cliOptions.name!, - gateway: cliOptions.gateway!, - lambdaArn: cliOptions.lambdaArn!, - toolSchemaFile: cliOptions.toolSchemaFile!, - }; - const result = await this.createLambdaFunctionArnTarget(config); - const output = { success: true, toolName: result.toolName }; - if (cliOptions.json) { - console.log(JSON.stringify(output)); - } else { - console.log(`Added gateway target '${result.toolName}'`); + // Handle Lambda Function ARN targets (no code generation) + if (cliOptions.type === 'lambdaFunctionArn') { + const config = { + targetType: 'lambdaFunctionArn' as const, + name: cliOptions.name!, + gateway: cliOptions.gateway!, + lambdaArn: cliOptions.lambdaArn!, + toolSchemaFile: cliOptions.toolSchemaFile!, + }; + const result = await this.createLambdaFunctionArnTarget(config); + const output = { success: true, toolName: result.toolName }; + if (cliOptions.json) { + console.log(JSON.stringify(output)); + } else { + console.log(`Added gateway target '${result.toolName}'`); + } + return { target_type: telemetryTargetType, host: 'lambda', outbound_auth: telemetryOutboundAuth }; } - process.exit(0); - } - // Handle MCP server targets (existing endpoint, no code generation) - if (cliOptions.type === 'mcpServer' && cliOptions.endpoint) { - const config: McpServerTargetConfig = { - targetType: 'mcpServer', - name: cliOptions.name!, - description: cliOptions.description ?? `Tool for ${cliOptions.name!}`, - endpoint: cliOptions.endpoint, - gateway: cliOptions.gateway!, - toolDefinition: { + // Handle MCP server targets (existing endpoint, no code generation) + if (cliOptions.type === 'mcpServer' && cliOptions.endpoint) { + const config: McpServerTargetConfig = { + targetType: 'mcpServer', name: cliOptions.name!, description: cliOptions.description ?? `Tool for ${cliOptions.name!}`, - inputSchema: { type: 'object' }, - }, - ...(cliOptions.outboundAuthType - ? { - outboundAuth: { - type: outboundAuthMap[cliOptions.outboundAuthType.toLowerCase()] ?? 'NONE', - credentialName: cliOptions.credentialName, - }, - } - : {}), - }; - const result = await this.createExternalGatewayTarget(config); - const output = { success: true, toolName: result.toolName, sourcePath: result.projectPath || undefined }; + endpoint: cliOptions.endpoint, + gateway: cliOptions.gateway!, + toolDefinition: { + name: cliOptions.name!, + description: cliOptions.description ?? `Tool for ${cliOptions.name!}`, + inputSchema: { type: 'object' }, + }, + ...(cliOptions.outboundAuthType + ? { + outboundAuth: { + type: outboundAuthMap[cliOptions.outboundAuthType.toLowerCase()] ?? 'NONE', + credentialName: cliOptions.credentialName, + }, + } + : {}), + }; + const result = await this.createExternalGatewayTarget(config); + const output = { + success: true, + toolName: result.toolName, + sourcePath: result.projectPath || undefined, + }; + if (cliOptions.json) { + console.log(JSON.stringify(output)); + } else { + console.log(`Added gateway target '${result.toolName}'`); + } + return { target_type: telemetryTargetType, host: telemetryHost, outbound_auth: telemetryOutboundAuth }; + } + + const result = await this.add({ + name: cliOptions.name!, + description: cliOptions.description, + language: cliOptions.language ?? 'Python', + gateway: cliOptions.gateway, + host: cliOptions.host, + }); + + if (!result.success) { + throw new Error(result.error); + } + if (cliOptions.json) { - console.log(JSON.stringify(output)); + console.log(JSON.stringify(result)); } else { console.log(`Added gateway target '${result.toolName}'`); + if (result.sourcePath) { + console.log(`Tool code: ${result.sourcePath}`); + } } - process.exit(0); - } - const result = await this.add({ - name: cliOptions.name!, - description: cliOptions.description, - language: cliOptions.language ?? 'Python', - gateway: cliOptions.gateway, - host: cliOptions.host, + return { target_type: telemetryTargetType, host: telemetryHost, outbound_auth: telemetryOutboundAuth }; }); - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else if (result.success) { - console.log(`Added gateway target '${result.toolName}'`); - if (result.sourcePath) { - console.log(`Tool code: ${result.sourcePath}`); - } - } else { - console.error(result.error); - } - - process.exit(result.success ? 0 : 1); + process.exit(0); } catch (error) { if (cliOptions.json) { console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); diff --git a/src/cli/primitives/MemoryPrimitive.tsx b/src/cli/primitives/MemoryPrimitive.tsx index ad157c2cf..42dffd6a4 100644 --- a/src/cli/primitives/MemoryPrimitive.tsx +++ b/src/cli/primitives/MemoryPrimitive.tsx @@ -17,6 +17,7 @@ import { import { DEFAULT_DELIVERY_TYPE, validateAddMemoryOptions } from '../commands/add/validate'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; import { requireTTY } from '../tui/guards/tty'; import { DEFAULT_EVENT_EXPIRY } from '../tui/screens/memory/types'; import { BasePrimitive } from './BasePrimitive'; @@ -191,44 +192,56 @@ export class MemoryPrimitive extends BasePrimitive { + const expiry = cliOptions.expiry ? parseInt(cliOptions.expiry, 10) : undefined; + const validation = validateAddMemoryOptions({ + name: cliOptions.name, + strategies: cliOptions.strategies, + expiry, + deliveryType: cliOptions.deliveryType, + dataStreamArn: cliOptions.dataStreamArn, + contentLevel: cliOptions.streamContentLevel, + streamDeliveryResources: cliOptions.streamDeliveryResources, + }); + + if (!validation.valid) { + throw new Error(validation.error); + } + + const result = await this.add({ + name: cliOptions.name!, + strategies: cliOptions.strategies, + expiry, + deliveryType: cliOptions.deliveryType, + dataStreamArn: cliOptions.dataStreamArn, + contentLevel: cliOptions.streamContentLevel, + streamDeliveryResources: cliOptions.streamDeliveryResources, + }); + + if (!result.success) { + throw new Error(result.error); + } - if (!validation.valid) { if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: validation.error })); + console.log(JSON.stringify(result)); } else { - console.error(validation.error); + console.log(`Added memory '${result.memoryName}'`); } - process.exit(1); - } - - const result = await this.add({ - name: cliOptions.name!, - strategies: cliOptions.strategies, - expiry, - deliveryType: cliOptions.deliveryType, - dataStreamArn: cliOptions.dataStreamArn, - contentLevel: cliOptions.streamContentLevel, - streamDeliveryResources: cliOptions.streamDeliveryResources, - }); - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else if (result.success) { - console.log(`Added memory '${result.memoryName}'`); - } else { - console.error(result.error); - } - process.exit(result.success ? 0 : 1); + const strategyList = (cliOptions.strategies ?? '') + .split(',') + .map(s => s.trim().toUpperCase()) + .filter(Boolean); + return { + strategy_count: strategyList.length, + strategy_semantic: strategyList.includes('SEMANTIC'), + strategy_summarization: strategyList.includes('SUMMARIZATION'), + strategy_user_preference: strategyList.includes('USER_PREFERENCE'), + strategy_episodic: strategyList.includes('EPISODIC'), + }; + }); + process.exit(0); } else { // TUI fallback — dynamic imports to avoid pulling ink (async) into registry requireTTY(); diff --git a/src/cli/primitives/OnlineEvalConfigPrimitive.ts b/src/cli/primitives/OnlineEvalConfigPrimitive.ts index c53bfb88f..44919aa71 100644 --- a/src/cli/primitives/OnlineEvalConfigPrimitive.ts +++ b/src/cli/primitives/OnlineEvalConfigPrimitive.ts @@ -3,6 +3,7 @@ import type { OnlineEvalConfig } from '../../schema'; import { OnlineEvalConfigSchema } from '../../schema'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; import { requireTTY } from '../tui/guards/tty'; import { BasePrimitive } from './BasePrimitive'; import type { AddResult, AddScreenComponent, RemovableResource } from './types'; @@ -131,45 +132,46 @@ export class OnlineEvalConfigPrimitive extends BasePrimitive { + if (!cliOptions.name || !cliOptions.runtime || allEvaluators.length === 0 || !cliOptions.samplingRate) { + throw new Error( + '--name, --runtime, --evaluator (and/or --evaluator-arn), and --sampling-rate are all required in non-interactive mode' + ); + } + + // Sampling rate as a percentage of requests to evaluate (0.01% to 100%) + const samplingRate = parseFloat(cliOptions.samplingRate); + if (isNaN(samplingRate) || samplingRate < 0.01 || samplingRate > 100) { + throw new Error( + `Invalid --sampling-rate "${cliOptions.samplingRate}". Must be a percentage between 0.01 and 100` + ); + } + + const result = await this.add({ + name: cliOptions.name, + agent: cliOptions.runtime, + evaluators: allEvaluators, + samplingRate, + enableOnCreate: cliOptions.enableOnCreate, + }); + + if (!result.success) { + throw new Error(result.error); } - process.exit(1); - } - // Sampling rate as a percentage of requests to evaluate (0.01% to 100%) - const samplingRate = parseFloat(cliOptions.samplingRate); - if (isNaN(samplingRate) || samplingRate < 0.01 || samplingRate > 100) { - const error = `Invalid --sampling-rate "${cliOptions.samplingRate}". Must be a percentage between 0.01 and 100`; if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error })); + console.log(JSON.stringify(result)); } else { - console.error(error); + console.log(`Added online eval config '${result.configName}'`); } - process.exit(1); - } - - const result = await this.add({ - name: cliOptions.name, - agent: cliOptions.runtime, - evaluators: allEvaluators, - samplingRate, - enableOnCreate: cliOptions.enableOnCreate, - }); - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else if (result.success) { - console.log(`Added online eval config '${result.configName}'`); - } else { - console.error(result.error); - } - process.exit(result.success ? 0 : 1); + return { + evaluator_count: allEvaluators.length, + enable_on_create: cliOptions.enableOnCreate ?? false, + }; + }); + process.exit(0); } else { // TUI fallback requireTTY(); diff --git a/src/cli/primitives/PolicyEnginePrimitive.ts b/src/cli/primitives/PolicyEnginePrimitive.ts index a1f887547..6efd141cc 100644 --- a/src/cli/primitives/PolicyEnginePrimitive.ts +++ b/src/cli/primitives/PolicyEnginePrimitive.ts @@ -3,6 +3,8 @@ import type { AgentCoreProjectSpec, PolicyEngine } from '../../schema'; import { PolicyEngineModeSchema, PolicyEngineSchema } from '../../schema'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { AttachMode, standardize } from '../telemetry/schemas/common-shapes.js'; import { requireTTY } from '../tui/guards/tty'; import { BasePrimitive } from './BasePrimitive'; import { SOURCE_CODE_NOTE } from './constants'; @@ -228,39 +230,50 @@ export class PolicyEnginePrimitive extends BasePrimitive { + if (!cliOptions.name) { + throw new Error('--name is required'); + } + + const result = await this.add({ + name: cliOptions.name, + description: cliOptions.description, + encryptionKeyArn: cliOptions.encryptionKeyArn, + }); + + // Attach to gateways if requested + if (result.success && cliOptions.attachToGateways) { + const mode = PolicyEngineModeSchema.parse(cliOptions.attachMode ?? 'LOG_ONLY'); + const gateways = cliOptions.attachToGateways + .split(',') + .map(s => s.trim()) + .filter(Boolean); + await this.attachToGateways(cliOptions.name, gateways, mode); + } + + if (!result.success) { + throw new Error(result.error); + } + if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: '--name is required' })); + console.log(JSON.stringify(result)); } else { - console.error('--name is required'); + console.log(`Added policy engine '${result.engineName}'`); } - process.exit(1); - } - const result = await this.add({ - name: cliOptions.name, - description: cliOptions.description, - encryptionKeyArn: cliOptions.encryptionKeyArn, + const gatewayCount = cliOptions.attachToGateways + ? cliOptions.attachToGateways + .split(',') + .map(s => s.trim()) + .filter(Boolean).length + : 0; + return { + attach_gateway_count: gatewayCount, + attach_mode: standardize(AttachMode, cliOptions.attachMode ?? 'log_only'), + }; }); - - // Attach to gateways if requested - if (result.success && cliOptions.attachToGateways) { - const mode = PolicyEngineModeSchema.parse(cliOptions.attachMode ?? 'LOG_ONLY'); - const gateways = cliOptions.attachToGateways - .split(',') - .map(s => s.trim()) - .filter(Boolean); - await this.attachToGateways(cliOptions.name, gateways, mode); - } - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else if (result.success) { - console.log(`Added policy engine '${result.engineName}'`); - } else { - console.error(result.error); - } - process.exit(result.success ? 0 : 1); + process.exit(0); } else { requireTTY(); const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ diff --git a/src/cli/primitives/PolicyPrimitive.ts b/src/cli/primitives/PolicyPrimitive.ts index bc1d446cc..202585983 100644 --- a/src/cli/primitives/PolicyPrimitive.ts +++ b/src/cli/primitives/PolicyPrimitive.ts @@ -5,6 +5,8 @@ import { detectRegion } from '../aws'; import { getPolicyGeneration, startPolicyGeneration } from '../aws/policy-generation'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { ValidationMode, standardize } from '../telemetry/schemas/common-shapes.js'; import { requireTTY } from '../tui/guards/tty'; import { BasePrimitive } from './BasePrimitive'; import { SOURCE_CODE_NOTE } from './constants'; @@ -305,44 +307,49 @@ export class PolicyPrimitive extends BasePrimitive { + if (!cliOptions.name) { + throw new Error('--name is required'); } - process.exit(1); - } - if (!cliOptions.engine) { + if (!cliOptions.engine) { + throw new Error('--engine is required'); + } + + const result = await this.add({ + name: cliOptions.name, + engine: cliOptions.engine, + description: cliOptions.description, + source: cliOptions.source, + statement: cliOptions.statement, + generate: cliOptions.generate, + gateway: cliOptions.gateway, + validationMode: cliOptions.validationMode + ? ValidationModeSchema.parse(cliOptions.validationMode) + : undefined, + }); + + if (!result.success) { + throw new Error(result.error); + } + if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: '--engine is required' })); + console.log(JSON.stringify(result)); } else { - console.error('--engine is required'); + console.log(`Added policy '${result.policyName}' to engine '${result.engineName}'`); } - process.exit(1); - } - const result = await this.add({ - name: cliOptions.name, - engine: cliOptions.engine, - description: cliOptions.description, - source: cliOptions.source, - statement: cliOptions.statement, - generate: cliOptions.generate, - gateway: cliOptions.gateway, - validationMode: cliOptions.validationMode - ? ValidationModeSchema.parse(cliOptions.validationMode) - : undefined, + const sourceType: 'file' | 'statement' | 'generate' = cliOptions.source + ? 'file' + : cliOptions.generate + ? 'generate' + : 'statement'; + return { + source_type: sourceType, + validation_mode: standardize(ValidationMode, cliOptions.validationMode ?? 'FAIL_ON_ANY_FINDINGS'), + }; }); - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else if (result.success) { - console.log(`Added policy '${result.policyName}' to engine '${result.engineName}'`); - } else { - console.error(result.error); - } - process.exit(result.success ? 0 : 1); + process.exit(0); } else { requireTTY(); const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ diff --git a/src/cli/primitives/RuntimeEndpointPrimitive.ts b/src/cli/primitives/RuntimeEndpointPrimitive.ts index 358b5fa69..aef1aecb6 100644 --- a/src/cli/primitives/RuntimeEndpointPrimitive.ts +++ b/src/cli/primitives/RuntimeEndpointPrimitive.ts @@ -4,6 +4,7 @@ import { RuntimeEndpointSchema } from '../../schema'; import type { ResourceType } from '../commands/remove/types'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; +import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; import { BasePrimitive } from './BasePrimitive'; import { SOURCE_CODE_NOTE } from './constants'; import type { AddResult, AddScreenComponent, RemovableResource } from './types'; @@ -254,22 +255,28 @@ export class RuntimeEndpointPrimitive extends BasePrimitive { + const result = await this.add({ + runtime: cliOptions.runtime, + endpoint: cliOptions.endpoint, + version: cliOptions.version, + description: cliOptions.description, + }); + + if (!result.success) { + throw new Error(result.error); + } + + if (cliOptions.json) { + console.log(JSON.stringify(result)); + } else { + console.log(`Added runtime endpoint '${cliOptions.endpoint}' to runtime '${cliOptions.runtime}'`); + } + + return {}; }); - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else if (result.success) { - console.log(`Added runtime endpoint '${cliOptions.endpoint}' to runtime '${cliOptions.runtime}'`); - } else { - console.error(result.error); - } - - process.exit(result.success ? 0 : 1); + process.exit(0); } catch (error) { if (cliOptions.json) { console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); diff --git a/src/cli/telemetry/schemas/command-run.ts b/src/cli/telemetry/schemas/command-run.ts index dfd127a98..0acfcaf1b 100644 --- a/src/cli/telemetry/schemas/command-run.ts +++ b/src/cli/telemetry/schemas/command-run.ts @@ -157,6 +157,7 @@ export const COMMAND_SCHEMAS = { 'add.gateway-target': AddGatewayTargetAttrs, 'add.policy-engine': AddPolicyEngineAttrs, 'add.policy': AddPolicyAttrs, + 'add.runtime-endpoint': NoAttrs, // deploy deploy: DeployAttrs, diff --git a/src/cli/telemetry/schemas/common-shapes.ts b/src/cli/telemetry/schemas/common-shapes.ts index 732bb3d61..3d52c28e3 100644 --- a/src/cli/telemetry/schemas/common-shapes.ts +++ b/src/cli/telemetry/schemas/common-shapes.ts @@ -26,6 +26,12 @@ export function resilientParse( return result; } +/** Lowercase a CLI value and parse it through a Zod enum, returning the narrowed type. */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function standardize>(schema: T, value: string): z.infer { + return schema.parse(value.toLowerCase()) as z.infer; +} + // Primitive types export const Count = z.number().int().nonnegative(); From a0eb4ecd6031232d992459f2202234491596e23c Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Wed, 29 Apr 2026 21:17:38 +0000 Subject: [PATCH 29/45] refactor: extract cliCommandRun helper, apply to all add.* primitives --- src/cli/primitives/AgentPrimitive.tsx | 153 ++++---- src/cli/primitives/CredentialPrimitive.tsx | 181 +++++---- src/cli/primitives/EvaluatorPrimitive.ts | 257 ++++++------- src/cli/primitives/GatewayPrimitive.ts | 114 +++--- src/cli/primitives/GatewayTargetPrimitive.ts | 349 +++++++++--------- src/cli/primitives/MemoryPrimitive.tsx | 159 ++++---- .../primitives/OnlineEvalConfigPrimitive.ts | 143 ++++--- src/cli/primitives/PolicyEnginePrimitive.ts | 137 ++++--- src/cli/primitives/PolicyPrimitive.ts | 153 ++++---- .../primitives/RuntimeEndpointPrimitive.ts | 53 ++- src/cli/telemetry/cli-command-run.ts | 26 ++ 11 files changed, 820 insertions(+), 905 deletions(-) create mode 100644 src/cli/telemetry/cli-command-run.ts diff --git a/src/cli/primitives/AgentPrimitive.tsx b/src/cli/primitives/AgentPrimitive.tsx index 77518dc58..7f94e1595 100644 --- a/src/cli/primitives/AgentPrimitive.tsx +++ b/src/cli/primitives/AgentPrimitive.tsx @@ -34,7 +34,7 @@ import { import { executeImportAgent } from '../operations/agent/import'; import { setupPythonProject } from '../operations/python'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; -import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { cliCommandRun } from '../telemetry/cli-command-run.js'; import { AgentType, AuthorizerType, @@ -277,93 +277,82 @@ export class AgentPrimitive extends BasePrimitive { - const validation = validateAddAgentOptions(cliOptions); - if (!validation.valid) { - throw new Error(validation.error); - } + await cliCommandRun('add.agent', !!cliOptions.json, async () => { + const validation = validateAddAgentOptions(cliOptions); + if (!validation.valid) { + throw new Error(validation.error); + } - // Parse custom claims JSON if provided (already validated by validateAddAgentOptions) - const customClaims = cliOptions.customClaims - ? (JSON.parse(cliOptions.customClaims) as CustomClaimValidation[]) - : undefined; - - // Parse request header allowlist if provided - const requestHeaderAllowlist = cliOptions.requestHeaderAllowlist - ? parseAndNormalizeHeaders(cliOptions.requestHeaderAllowlist) - : undefined; - - const result = await this.add({ - name: cliOptions.name!, - type: cliOptions.type ?? 'create', - buildType: (cliOptions.build as BuildType) ?? 'CodeZip', - language: cliOptions.language!, - framework: cliOptions.framework!, - modelProvider: cliOptions.modelProvider!, - apiKey: cliOptions.apiKey, - memory: cliOptions.memory, - protocol: cliOptions.protocol, - networkMode: cliOptions.networkMode, - subnets: cliOptions.subnets, - securityGroups: cliOptions.securityGroups, - requestHeaderAllowlist, - codeLocation: cliOptions.codeLocation, - entrypoint: cliOptions.entrypoint, - bedrockAgentId: cliOptions.agentId, - bedrockAliasId: cliOptions.agentAliasId, - bedrockRegion: cliOptions.region, - authorizerType: cliOptions.authorizerType, - discoveryUrl: cliOptions.discoveryUrl, - allowedAudience: cliOptions.allowedAudience, - allowedClients: cliOptions.allowedClients, - allowedScopes: cliOptions.allowedScopes, - customClaims, - clientId: cliOptions.clientId, - clientSecret: cliOptions.clientSecret, - idleTimeout: cliOptions.idleTimeout ? Number(cliOptions.idleTimeout) : undefined, - maxLifetime: cliOptions.maxLifetime ? Number(cliOptions.maxLifetime) : undefined, - sessionStorageMountPath: cliOptions.sessionStorageMountPath, - }); - - if (!result.success) { - throw new Error(result.error); - } + // Parse custom claims JSON if provided (already validated by validateAddAgentOptions) + const customClaims = cliOptions.customClaims + ? (JSON.parse(cliOptions.customClaims) as CustomClaimValidation[]) + : undefined; + + // Parse request header allowlist if provided + const requestHeaderAllowlist = cliOptions.requestHeaderAllowlist + ? parseAndNormalizeHeaders(cliOptions.requestHeaderAllowlist) + : undefined; + + const result = await this.add({ + name: cliOptions.name!, + type: cliOptions.type ?? 'create', + buildType: (cliOptions.build as BuildType) ?? 'CodeZip', + language: cliOptions.language!, + framework: cliOptions.framework!, + modelProvider: cliOptions.modelProvider!, + apiKey: cliOptions.apiKey, + memory: cliOptions.memory, + protocol: cliOptions.protocol, + networkMode: cliOptions.networkMode, + subnets: cliOptions.subnets, + securityGroups: cliOptions.securityGroups, + requestHeaderAllowlist, + codeLocation: cliOptions.codeLocation, + entrypoint: cliOptions.entrypoint, + bedrockAgentId: cliOptions.agentId, + bedrockAliasId: cliOptions.agentAliasId, + bedrockRegion: cliOptions.region, + authorizerType: cliOptions.authorizerType, + discoveryUrl: cliOptions.discoveryUrl, + allowedAudience: cliOptions.allowedAudience, + allowedClients: cliOptions.allowedClients, + allowedScopes: cliOptions.allowedScopes, + customClaims, + clientId: cliOptions.clientId, + clientSecret: cliOptions.clientSecret, + idleTimeout: cliOptions.idleTimeout ? Number(cliOptions.idleTimeout) : undefined, + maxLifetime: cliOptions.maxLifetime ? Number(cliOptions.maxLifetime) : undefined, + sessionStorageMountPath: cliOptions.sessionStorageMountPath, + }); - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else { - console.log(`Added agent '${result.agentName}'`); - if (result.agentPath) { - console.log(`Agent code: ${result.agentPath}`); - } - if (cliOptions.networkMode === 'VPC') { - console.log(`\x1b[33mNote: ${VPC_ENDPOINT_WARNING}\x1b[0m`); - } - } + if (!result.success) { + throw new Error(result.error); + } - return { - language: standardize(Language, cliOptions.language!), - framework: standardize(Framework, cliOptions.framework!), - model_provider: standardize(ModelProviderEnum, cliOptions.modelProvider!), - agent_type: standardize(AgentType, cliOptions.type ?? 'create'), - build: standardize(Build, cliOptions.build ?? 'CodeZip'), - protocol: standardize(Protocol, cliOptions.protocol ?? 'HTTP'), - network_mode: standardize(NetworkModeEnum, cliOptions.networkMode ?? 'PUBLIC'), - authorizer_type: standardize(AuthorizerType, cliOptions.authorizerType ?? 'NONE'), - memory: standardize(Memory, cliOptions.memory ?? 'none'), - }; - }); - process.exit(0); - } catch (error) { if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); + console.log(JSON.stringify(result)); } else { - console.error(getErrorMessage(error)); + console.log(`Added agent '${result.agentName}'`); + if (result.agentPath) { + console.log(`Agent code: ${result.agentPath}`); + } + if (cliOptions.networkMode === 'VPC') { + console.log(`\x1b[33mNote: ${VPC_ENDPOINT_WARNING}\x1b[0m`); + } } - process.exit(1); - } + + return { + language: standardize(Language, cliOptions.language!), + framework: standardize(Framework, cliOptions.framework!), + model_provider: standardize(ModelProviderEnum, cliOptions.modelProvider!), + agent_type: standardize(AgentType, cliOptions.type ?? 'create'), + build: standardize(Build, cliOptions.build ?? 'CodeZip'), + protocol: standardize(Protocol, cliOptions.protocol ?? 'HTTP'), + network_mode: standardize(NetworkModeEnum, cliOptions.networkMode ?? 'PUBLIC'), + authorizer_type: standardize(AuthorizerType, cliOptions.authorizerType ?? 'NONE'), + memory: standardize(Memory, cliOptions.memory ?? 'none'), + }; + }); } else { // TUI fallback — dynamic imports to avoid pulling ink (async) into registry requireTTY(); diff --git a/src/cli/primitives/CredentialPrimitive.tsx b/src/cli/primitives/CredentialPrimitive.tsx index ba9ddfc2d..e9bf61e24 100644 --- a/src/cli/primitives/CredentialPrimitive.tsx +++ b/src/cli/primitives/CredentialPrimitive.tsx @@ -4,7 +4,7 @@ import { CredentialSchema } from '../../schema'; import { validateAddCredentialOptions } from '../commands/add/validate'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; -import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { cliCommandRun } from '../telemetry/cli-command-run.js'; import { CredentialType, standardize } from '../telemetry/schemas/common-shapes.js'; import { requireTTY } from '../tui/guards/tty'; import { BasePrimitive } from './BasePrimitive'; @@ -275,103 +275,92 @@ export class CredentialPrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } - - if ( - cliOptions.name || - cliOptions.apiKey || - cliOptions.json || - cliOptions.type || - cliOptions.discoveryUrl || - cliOptions.clientId || - cliOptions.clientSecret || - cliOptions.scopes - ) { - // CLI mode - const client = await TelemetryClientAccessor.get(); - await client.withCommandRun('add.credential', async () => { - const validation = validateAddCredentialOptions({ - name: cliOptions.name, - type: cliOptions.type as 'api-key' | 'oauth' | undefined, - apiKey: cliOptions.apiKey, - discoveryUrl: cliOptions.discoveryUrl, - clientId: cliOptions.clientId, - clientSecret: cliOptions.clientSecret, - scopes: cliOptions.scopes, - }); - - if (!validation.valid) { - throw new Error(validation.error); - } - - const addOptions = - cliOptions.type === 'oauth' - ? { - authorizerType: 'OAuthCredentialProvider' as const, - name: cliOptions.name!, - discoveryUrl: cliOptions.discoveryUrl!, - clientId: cliOptions.clientId!, - clientSecret: cliOptions.clientSecret!, - scopes: cliOptions.scopes - ?.split(',') - .map(s => s.trim()) - .filter(Boolean), - } - : { - authorizerType: 'ApiKeyCredentialProvider' as const, - name: cliOptions.name!, - apiKey: cliOptions.apiKey!, - }; - - const result = await this.add(addOptions); - - if (!result.success) { - throw new Error(result.error); - } - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else { - console.log(`Added credential '${result.credentialName}'`); - } - - return { - credential_type: standardize(CredentialType, cliOptions.type ?? 'api-key'), - }; - }); - process.exit(0); - } else { - // TUI fallback — dynamic imports to avoid pulling ink (async) into registry - requireTTY(); - const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ - import('ink'), - import('react'), - import('../tui/screens/add/AddFlow'), - ]); - const { clear, unmount } = render( - React.createElement(AddFlow, { - isInteractive: false, - initialResource: 'credential', - onExit: () => { - clear(); - unmount(); - process.exit(0); - }, - }) - ); - } - } catch (error) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); - } else { - console.error(getErrorMessage(error)); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); process.exit(1); } + + if ( + cliOptions.name || + cliOptions.apiKey || + cliOptions.json || + cliOptions.type || + cliOptions.discoveryUrl || + cliOptions.clientId || + cliOptions.clientSecret || + cliOptions.scopes + ) { + // CLI mode + await cliCommandRun('add.credential', !!cliOptions.json, async () => { + const validation = validateAddCredentialOptions({ + name: cliOptions.name, + type: cliOptions.type as 'api-key' | 'oauth' | undefined, + apiKey: cliOptions.apiKey, + discoveryUrl: cliOptions.discoveryUrl, + clientId: cliOptions.clientId, + clientSecret: cliOptions.clientSecret, + scopes: cliOptions.scopes, + }); + + if (!validation.valid) { + throw new Error(validation.error); + } + + const addOptions = + cliOptions.type === 'oauth' + ? { + authorizerType: 'OAuthCredentialProvider' as const, + name: cliOptions.name!, + discoveryUrl: cliOptions.discoveryUrl!, + clientId: cliOptions.clientId!, + clientSecret: cliOptions.clientSecret!, + scopes: cliOptions.scopes + ?.split(',') + .map(s => s.trim()) + .filter(Boolean), + } + : { + authorizerType: 'ApiKeyCredentialProvider' as const, + name: cliOptions.name!, + apiKey: cliOptions.apiKey!, + }; + + const result = await this.add(addOptions); + + if (!result.success) { + throw new Error(result.error); + } + + if (cliOptions.json) { + console.log(JSON.stringify(result)); + } else { + console.log(`Added credential '${result.credentialName}'`); + } + + return { + credential_type: standardize(CredentialType, cliOptions.type ?? 'api-key'), + }; + }); + } else { + // TUI fallback — dynamic imports to avoid pulling ink (async) into registry + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + initialResource: 'credential', + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + } } ); diff --git a/src/cli/primitives/EvaluatorPrimitive.ts b/src/cli/primitives/EvaluatorPrimitive.ts index 355bd0ce9..f8a7d3fa2 100644 --- a/src/cli/primitives/EvaluatorPrimitive.ts +++ b/src/cli/primitives/EvaluatorPrimitive.ts @@ -3,7 +3,7 @@ import type { EvaluationLevel, Evaluator, EvaluatorConfig } from '../../schema'; import { EvaluationLevelSchema, EvaluatorSchema } from '../../schema'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; -import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { cliCommandRun } from '../telemetry/cli-command-run.js'; import { EvaluatorType, Level, standardize } from '../telemetry/schemas/common-shapes.js'; import { renderCodeBasedEvaluatorTemplate } from '../templates/EvaluatorRenderer'; import { requireTTY } from '../tui/guards/tty'; @@ -198,157 +198,146 @@ export class EvaluatorPrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } - - if (cliOptions.name || cliOptions.json) { - const client = await TelemetryClientAccessor.get(); - await client.withCommandRun('add.evaluator', async () => { - const fail = (error: string): never => { - throw new Error(error); - }; - - if (!cliOptions.name || !cliOptions.level) { - fail('--name and --level are required in non-interactive mode'); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } - const levelResult = EvaluationLevelSchema.safeParse(cliOptions.level); - if (!levelResult.success) { - fail(`Invalid --level "${cliOptions.level}". Must be one of: SESSION, TRACE, TOOL_CALL`); + if (cliOptions.name || cliOptions.json) { + await cliCommandRun('add.evaluator', !!cliOptions.json, async () => { + const fail = (error: string): never => { + throw new Error(error); + }; + + if (!cliOptions.name || !cliOptions.level) { + fail('--name and --level are required in non-interactive mode'); + } + + const levelResult = EvaluationLevelSchema.safeParse(cliOptions.level); + if (!levelResult.success) { + fail(`Invalid --level "${cliOptions.level}". Must be one of: SESSION, TRACE, TOOL_CALL`); + } + + const evalType = cliOptions.type ?? 'llm-as-a-judge'; + if (evalType !== 'llm-as-a-judge' && evalType !== 'code-based') { + fail(`Invalid --type "${evalType}". Must be one of: llm-as-a-judge, code-based`); + } + + // Cross-validate flags against evaluator type + if (evalType !== 'code-based') { + if (cliOptions.lambdaArn) fail('--lambda-arn requires --type code-based'); + if (cliOptions.timeout) fail('--timeout requires --type code-based'); + } + if (evalType === 'code-based') { + if (cliOptions.model) fail('--model cannot be used with --type code-based'); + if (cliOptions.instructions) fail('--instructions cannot be used with --type code-based'); + if (cliOptions.ratingScale) fail('--rating-scale cannot be used with --type code-based'); + } + + let configJson: EvaluatorConfig; + + if (cliOptions.config) { + const { readFileSync } = await import('fs'); + configJson = JSON.parse(readFileSync(cliOptions.config, 'utf-8')) as EvaluatorConfig; + } else if (evalType === 'code-based') { + configJson = this.buildCodeBasedConfig(cliOptions.name!, cliOptions.lambdaArn, cliOptions.timeout); + } else { + // LLM-as-a-Judge flow + if (!cliOptions.model) { + fail('Either --config or --model is required for LLM-as-a-Judge evaluators'); } - const evalType = cliOptions.type ?? 'llm-as-a-judge'; - if (evalType !== 'llm-as-a-judge' && evalType !== 'code-based') { - fail(`Invalid --type "${evalType}". Must be one of: llm-as-a-judge, code-based`); + if (!cliOptions.instructions) { + const level = levelResult.data!; + const placeholders = LEVEL_PLACEHOLDERS[level].map(p => `{${p}}`).join(', '); + fail( + `--instructions is required in non-interactive mode (or use --config). ` + + `Must include at least one placeholder for ${level}: ${placeholders}` + ); } - // Cross-validate flags against evaluator type - if (evalType !== 'code-based') { - if (cliOptions.lambdaArn) fail('--lambda-arn requires --type code-based'); - if (cliOptions.timeout) fail('--timeout requires --type code-based'); - } - if (evalType === 'code-based') { - if (cliOptions.model) fail('--model cannot be used with --type code-based'); - if (cliOptions.instructions) fail('--instructions cannot be used with --type code-based'); - if (cliOptions.ratingScale) fail('--rating-scale cannot be used with --type code-based'); + const placeholderCheck = validateInstructionPlaceholders(cliOptions.instructions!, levelResult.data!); + if (placeholderCheck !== true) { + fail(placeholderCheck); } - let configJson: EvaluatorConfig; + let ratingScale: NonNullable['ratingScale']; + const scaleInput = cliOptions.ratingScale ?? '1-5-quality'; - if (cliOptions.config) { - const { readFileSync } = await import('fs'); - configJson = JSON.parse(readFileSync(cliOptions.config, 'utf-8')) as EvaluatorConfig; - } else if (evalType === 'code-based') { - configJson = this.buildCodeBasedConfig(cliOptions.name!, cliOptions.lambdaArn, cliOptions.timeout); + const preset = RATING_SCALE_PRESETS.find(p => p.id === scaleInput); + if (preset) { + ratingScale = preset.ratingScale; } else { - // LLM-as-a-Judge flow - if (!cliOptions.model) { - fail('Either --config or --model is required for LLM-as-a-Judge evaluators'); - } - - if (!cliOptions.instructions) { - const level = levelResult.data!; - const placeholders = LEVEL_PLACEHOLDERS[level].map(p => `{${p}}`).join(', '); + const isNumerical = /^\d/.test(scaleInput.trim()); + const parsed = parseCustomRatingScale(scaleInput, isNumerical ? 'numerical' : 'categorical'); + if (!parsed.success) { fail( - `--instructions is required in non-interactive mode (or use --config). ` + - `Must include at least one placeholder for ${level}: ${placeholders}` + `Invalid --rating-scale "${scaleInput}". Use a preset (${presetIds.join(', ')}) ` + + `or custom format: "1:Label:Definition, 2:Label:Definition" (numerical) ` + + `or "Label:Definition, Label:Definition" (categorical)` ); } - - const placeholderCheck = validateInstructionPlaceholders(cliOptions.instructions!, levelResult.data!); - if (placeholderCheck !== true) { - fail(placeholderCheck); - } - - let ratingScale: NonNullable['ratingScale']; - const scaleInput = cliOptions.ratingScale ?? '1-5-quality'; - - const preset = RATING_SCALE_PRESETS.find(p => p.id === scaleInput); - if (preset) { - ratingScale = preset.ratingScale; - } else { - const isNumerical = /^\d/.test(scaleInput.trim()); - const parsed = parseCustomRatingScale(scaleInput, isNumerical ? 'numerical' : 'categorical'); - if (!parsed.success) { - fail( - `Invalid --rating-scale "${scaleInput}". Use a preset (${presetIds.join(', ')}) ` + - `or custom format: "1:Label:Definition, 2:Label:Definition" (numerical) ` + - `or "Label:Definition, Label:Definition" (categorical)` - ); - } - ratingScale = parsed.success ? parsed.ratingScale : undefined!; - } - - configJson = { - llmAsAJudge: { - model: cliOptions.model!, - instructions: cliOptions.instructions!, - ratingScale, - }, - }; + ratingScale = parsed.success ? parsed.ratingScale : undefined!; } - const result = await this.add({ - name: cliOptions.name!, - level: levelResult.data!, - config: configJson, - }); + configJson = { + llmAsAJudge: { + model: cliOptions.model!, + instructions: cliOptions.instructions!, + ratingScale, + }, + }; + } - if (!result.success) { - throw new Error(result.error); - } + const result = await this.add({ + name: cliOptions.name!, + level: levelResult.data!, + config: configJson, + }); - if (cliOptions.json) { - console.log(JSON.stringify(result)); + if (!result.success) { + throw new Error(result.error); + } + + if (cliOptions.json) { + console.log(JSON.stringify(result)); + } else { + if (result.codePath) { + console.log(`Created evaluator '${result.evaluatorName}'`); + console.log(` Code: ${result.codePath}lambda_function.py`); + console.log(` IAM: ${result.codePath}execution-role-policy.json`); + console.log( + `\n Next: Edit lambda_function.py with your evaluation logic, then run \`agentcore deploy\`` + ); } else { - if (result.codePath) { - console.log(`Created evaluator '${result.evaluatorName}'`); - console.log(` Code: ${result.codePath}lambda_function.py`); - console.log(` IAM: ${result.codePath}execution-role-policy.json`); - console.log( - `\n Next: Edit lambda_function.py with your evaluation logic, then run \`agentcore deploy\`` - ); - } else { - console.log(`Added evaluator '${result.evaluatorName}'`); - } + console.log(`Added evaluator '${result.evaluatorName}'`); } - - return { - evaluator_type: standardize(EvaluatorType, evalType), - level: standardize(Level, levelResult.data!), - }; - }); - process.exit(0); - } else { - // TUI fallback - requireTTY(); - const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ - import('ink'), - import('react'), - import('../tui/screens/add/AddFlow'), - ]); - const { clear, unmount } = render( - React.createElement(AddFlow, { - isInteractive: false, - initialResource: 'evaluator', - onExit: () => { - clear(); - unmount(); - process.exit(0); - }, - }) - ); - } - } catch (error) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); - } else { - console.error(getErrorMessage(error)); - } - process.exit(1); + } + + return { + evaluator_type: standardize(EvaluatorType, evalType), + level: standardize(Level, levelResult.data!), + }; + }); + } else { + // TUI fallback + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + initialResource: 'evaluator', + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); } } ); diff --git a/src/cli/primitives/GatewayPrimitive.ts b/src/cli/primitives/GatewayPrimitive.ts index 04be5b473..625a3c4a5 100644 --- a/src/cli/primitives/GatewayPrimitive.ts +++ b/src/cli/primitives/GatewayPrimitive.ts @@ -12,7 +12,7 @@ import type { AddGatewayOptions as CLIAddGatewayOptions } from '../commands/add/ import { validateAddGatewayOptions } from '../commands/add/validate'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; -import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { cliCommandRun } from '../telemetry/cli-command-run.js'; import { AuthorizerType, PolicyEngineMode, standardize } from '../telemetry/schemas/common-shapes.js'; import { requireTTY } from '../tui/guards/tty'; import type { AddGatewayConfig } from '../tui/screens/mcp/types'; @@ -184,75 +184,63 @@ export class GatewayPrimitive extends BasePrimitive) => { const cliOptions = rawOptions as unknown as CLIAddGatewayOptions; - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } + await cliCommandRun('add.gateway', !!cliOptions.json, async () => { + const validation = validateAddGatewayOptions(cliOptions); + if (!validation.valid) { + throw new Error(validation.error); } - const client = await TelemetryClientAccessor.get(); - await client.withCommandRun('add.gateway', async () => { - const validation = validateAddGatewayOptions(cliOptions); - if (!validation.valid) { - throw new Error(validation.error); - } - - // Parse custom claims JSON if provided (already validated) - const parsedCustomClaims = cliOptions.customClaims - ? (JSON.parse(cliOptions.customClaims) as CustomClaimValidation[]) - : undefined; - - const result = await this.add({ - name: cliOptions.name!, - description: cliOptions.description, - authorizerType: cliOptions.authorizerType ?? 'NONE', - discoveryUrl: cliOptions.discoveryUrl, - allowedAudience: cliOptions.allowedAudience, - allowedClients: cliOptions.allowedClients, - allowedScopes: cliOptions.allowedScopes, - customClaims: parsedCustomClaims, - clientId: cliOptions.clientId, - clientSecret: cliOptions.clientSecret, - runtimes: cliOptions.runtimes, - enableSemanticSearch: cliOptions.semanticSearch !== false, - exceptionLevel: cliOptions.exceptionLevel, - policyEngine: cliOptions.policyEngine, - policyEngineMode: cliOptions.policyEngineMode, - }); - - if (!result.success) { - throw new Error(result.error); - } + // Parse custom claims JSON if provided (already validated) + const parsedCustomClaims = cliOptions.customClaims + ? (JSON.parse(cliOptions.customClaims) as CustomClaimValidation[]) + : undefined; + + const result = await this.add({ + name: cliOptions.name!, + description: cliOptions.description, + authorizerType: cliOptions.authorizerType ?? 'NONE', + discoveryUrl: cliOptions.discoveryUrl, + allowedAudience: cliOptions.allowedAudience, + allowedClients: cliOptions.allowedClients, + allowedScopes: cliOptions.allowedScopes, + customClaims: parsedCustomClaims, + clientId: cliOptions.clientId, + clientSecret: cliOptions.clientSecret, + runtimes: cliOptions.runtimes, + enableSemanticSearch: cliOptions.semanticSearch !== false, + exceptionLevel: cliOptions.exceptionLevel, + policyEngine: cliOptions.policyEngine, + policyEngineMode: cliOptions.policyEngineMode, + }); - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else { - console.log(`Added gateway '${result.gatewayName}'`); - } + if (!result.success) { + throw new Error(result.error); + } - const runtimeCount = cliOptions.runtimes - ? cliOptions.runtimes - .split(',') - .map(s => s.trim()) - .filter(Boolean).length - : 0; - return { - authorizer_type: standardize(AuthorizerType, cliOptions.authorizerType ?? 'NONE'), - has_policy_engine: !!cliOptions.policyEngine, - policy_engine_mode: standardize(PolicyEngineMode, cliOptions.policyEngineMode ?? 'log_only'), - semantic_search: cliOptions.semanticSearch !== false, - runtime_count: runtimeCount, - }; - }); - process.exit(0); - } catch (error) { if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); + console.log(JSON.stringify(result)); } else { - console.error(`Error: ${getErrorMessage(error)}`); + console.log(`Added gateway '${result.gatewayName}'`); } - process.exit(1); - } + + const runtimeCount = cliOptions.runtimes + ? cliOptions.runtimes + .split(',') + .map(s => s.trim()) + .filter(Boolean).length + : 0; + return { + authorizer_type: standardize(AuthorizerType, cliOptions.authorizerType ?? 'NONE'), + has_policy_engine: !!cliOptions.policyEngine, + policy_engine_mode: standardize(PolicyEngineMode, cliOptions.policyEngineMode ?? 'log_only'), + semantic_search: cliOptions.semanticSearch !== false, + runtime_count: runtimeCount, + }; + }); }); removeCmd diff --git a/src/cli/primitives/GatewayTargetPrimitive.ts b/src/cli/primitives/GatewayTargetPrimitive.ts index 09c146fbb..2323a37a0 100644 --- a/src/cli/primitives/GatewayTargetPrimitive.ts +++ b/src/cli/primitives/GatewayTargetPrimitive.ts @@ -14,7 +14,7 @@ import { validateAddGatewayTargetOptions } from '../commands/add/validate'; import { getErrorMessage } from '../errors'; import type { RemovableGatewayTarget } from '../operations/remove/remove-gateway-target'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; -import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { cliCommandRun } from '../telemetry/cli-command-run.js'; import { GatewayTargetHost, OutboundAuth, standardize } from '../telemetry/schemas/common-shapes.js'; import { getTemplateToolDefinitions, renderGatewayTargetTemplate } from '../templates/GatewayTargetRenderer'; import { requireTTY } from '../tui/guards/tty'; @@ -299,208 +299,197 @@ export class GatewayTargetPrimitive extends BasePrimitive { - const validation = await validateAddGatewayTargetOptions(cliOptions); - if (!validation.valid) { - throw new Error(validation.error); - } + await cliCommandRun('add.gateway-target', !!cliOptions.json, async () => { + const validation = await validateAddGatewayTargetOptions(cliOptions); + if (!validation.valid) { + throw new Error(validation.error); + } - // Map CLI flag values to internal types - const outboundAuthMap: Record = { - oauth: 'OAUTH', - 'api-key': 'API_KEY', - api_key: 'API_KEY', - none: 'NONE', + // Map CLI flag values to internal types + const outboundAuthMap: Record = { + oauth: 'OAUTH', + 'api-key': 'API_KEY', + api_key: 'API_KEY', + none: 'NONE', + }; + + // Map target type from camelCase CLI to kebab-case telemetry + const targetTypeMap = { + apiGateway: 'api-gateway', + openApiSchema: 'open-api-schema', + smithyModel: 'smithy-model', + lambdaFunctionArn: 'lambda-function-arn', + mcpServer: 'mcp-server', + } as const; + type TargetTypeKey = keyof typeof targetTypeMap; + + const cliType = cliOptions.type ?? ''; + const telemetryTargetType = + cliType in targetTypeMap ? targetTypeMap[cliType as TargetTypeKey] : ('mcp-server' as const); + const telemetryOutboundAuth = standardize( + OutboundAuth, + (cliOptions.outboundAuthType ?? 'none').replace('_', '-') + ); + const telemetryHost = standardize(GatewayTargetHost, cliOptions.host ?? 'lambda'); + + // Handle API Gateway targets (no code generation) + if (cliOptions.type === 'apiGateway') { + const config: ApiGatewayTargetConfig = { + targetType: 'apiGateway', + name: cliOptions.name!, + gateway: cliOptions.gateway!, + restApiId: cliOptions.restApiId!, + stage: cliOptions.stage!, + toolFilters: cliOptions.toolFilterPath + ? [ + { + filterPath: cliOptions.toolFilterPath, + methods: (cliOptions.toolFilterMethods?.split(',').map(m => m.trim()) ?? [ + 'GET', + ]) as ApiGatewayHttpMethod[], + }, + ] + : undefined, + ...(cliOptions.outboundAuthType + ? { + outboundAuth: { + type: (outboundAuthMap[cliOptions.outboundAuthType.toLowerCase()] ?? 'NONE') as + | 'API_KEY' + | 'NONE', + credentialName: cliOptions.credentialName, + }, + } + : {}), }; - - // Map target type from camelCase CLI to kebab-case telemetry - const targetTypeMap = { - apiGateway: 'api-gateway', - openApiSchema: 'open-api-schema', - smithyModel: 'smithy-model', - lambdaFunctionArn: 'lambda-function-arn', - mcpServer: 'mcp-server', - } as const; - type TargetTypeKey = keyof typeof targetTypeMap; - - const cliType = cliOptions.type ?? ''; - const telemetryTargetType = - cliType in targetTypeMap ? targetTypeMap[cliType as TargetTypeKey] : ('mcp-server' as const); - const telemetryOutboundAuth = standardize( - OutboundAuth, - (cliOptions.outboundAuthType ?? 'none').replace('_', '-') - ); - const telemetryHost = standardize(GatewayTargetHost, cliOptions.host ?? 'lambda'); - - // Handle API Gateway targets (no code generation) - if (cliOptions.type === 'apiGateway') { - const config: ApiGatewayTargetConfig = { - targetType: 'apiGateway', - name: cliOptions.name!, - gateway: cliOptions.gateway!, - restApiId: cliOptions.restApiId!, - stage: cliOptions.stage!, - toolFilters: cliOptions.toolFilterPath - ? [ - { - filterPath: cliOptions.toolFilterPath, - methods: (cliOptions.toolFilterMethods?.split(',').map(m => m.trim()) ?? [ - 'GET', - ]) as ApiGatewayHttpMethod[], - }, - ] - : undefined, - ...(cliOptions.outboundAuthType - ? { - outboundAuth: { - type: (outboundAuthMap[cliOptions.outboundAuthType.toLowerCase()] ?? 'NONE') as - | 'API_KEY' - | 'NONE', - credentialName: cliOptions.credentialName, - }, - } - : {}), - }; - const result = await this.createApiGatewayTarget(config); - const output = { success: true, toolName: result.toolName }; - if (cliOptions.json) { - console.log(JSON.stringify(output)); - } else { - console.log(`Added gateway target '${result.toolName}'`); - } - return { target_type: telemetryTargetType, host: telemetryHost, outbound_auth: telemetryOutboundAuth }; + const result = await this.createApiGatewayTarget(config); + const output = { success: true, toolName: result.toolName }; + if (cliOptions.json) { + console.log(JSON.stringify(output)); + } else { + console.log(`Added gateway target '${result.toolName}'`); } + return { target_type: telemetryTargetType, host: telemetryHost, outbound_auth: telemetryOutboundAuth }; + } - // Handle schema-based targets (OpenAPI / Smithy) - if ((cliOptions.type === 'openApiSchema' || cliOptions.type === 'smithyModel') && cliOptions.schema) { - const isS3 = cliOptions.schema.startsWith('s3://'); - const schemaSource = isS3 + // Handle schema-based targets (OpenAPI / Smithy) + if ((cliOptions.type === 'openApiSchema' || cliOptions.type === 'smithyModel') && cliOptions.schema) { + const isS3 = cliOptions.schema.startsWith('s3://'); + const schemaSource = isS3 + ? { + s3: { + uri: cliOptions.schema, + ...(cliOptions.schemaS3Account ? { bucketOwnerAccountId: cliOptions.schemaS3Account } : {}), + }, + } + : { inline: { path: cliOptions.schema } }; + + const config: SchemaBasedTargetConfig = { + name: cliOptions.name!, + targetType: cliOptions.type, + schemaSource, + gateway: cliOptions.gateway!, + ...(cliOptions.outboundAuthType ? { - s3: { - uri: cliOptions.schema, - ...(cliOptions.schemaS3Account ? { bucketOwnerAccountId: cliOptions.schemaS3Account } : {}), + outboundAuth: { + type: outboundAuthMap[cliOptions.outboundAuthType.toLowerCase()] ?? 'NONE', + credentialName: cliOptions.credentialName, }, } - : { inline: { path: cliOptions.schema } }; - - const config: SchemaBasedTargetConfig = { - name: cliOptions.name!, - targetType: cliOptions.type, - schemaSource, - gateway: cliOptions.gateway!, - ...(cliOptions.outboundAuthType - ? { - outboundAuth: { - type: outboundAuthMap[cliOptions.outboundAuthType.toLowerCase()] ?? 'NONE', - credentialName: cliOptions.credentialName, - }, - } - : {}), - }; - const result = await this.createSchemaBasedGatewayTarget(config); - const output = { success: true, toolName: result.toolName }; - if (cliOptions.json) { - console.log(JSON.stringify(output)); - } else { - console.log(`Added gateway target '${result.toolName}'`); - } - return { target_type: telemetryTargetType, host: telemetryHost, outbound_auth: telemetryOutboundAuth }; + : {}), + }; + const result = await this.createSchemaBasedGatewayTarget(config); + const output = { success: true, toolName: result.toolName }; + if (cliOptions.json) { + console.log(JSON.stringify(output)); + } else { + console.log(`Added gateway target '${result.toolName}'`); } + return { target_type: telemetryTargetType, host: telemetryHost, outbound_auth: telemetryOutboundAuth }; + } - // Handle Lambda Function ARN targets (no code generation) - if (cliOptions.type === 'lambdaFunctionArn') { - const config = { - targetType: 'lambdaFunctionArn' as const, - name: cliOptions.name!, - gateway: cliOptions.gateway!, - lambdaArn: cliOptions.lambdaArn!, - toolSchemaFile: cliOptions.toolSchemaFile!, - }; - const result = await this.createLambdaFunctionArnTarget(config); - const output = { success: true, toolName: result.toolName }; - if (cliOptions.json) { - console.log(JSON.stringify(output)); - } else { - console.log(`Added gateway target '${result.toolName}'`); - } - return { target_type: telemetryTargetType, host: 'lambda', outbound_auth: telemetryOutboundAuth }; + // Handle Lambda Function ARN targets (no code generation) + if (cliOptions.type === 'lambdaFunctionArn') { + const config = { + targetType: 'lambdaFunctionArn' as const, + name: cliOptions.name!, + gateway: cliOptions.gateway!, + lambdaArn: cliOptions.lambdaArn!, + toolSchemaFile: cliOptions.toolSchemaFile!, + }; + const result = await this.createLambdaFunctionArnTarget(config); + const output = { success: true, toolName: result.toolName }; + if (cliOptions.json) { + console.log(JSON.stringify(output)); + } else { + console.log(`Added gateway target '${result.toolName}'`); } + return { target_type: telemetryTargetType, host: 'lambda', outbound_auth: telemetryOutboundAuth }; + } - // Handle MCP server targets (existing endpoint, no code generation) - if (cliOptions.type === 'mcpServer' && cliOptions.endpoint) { - const config: McpServerTargetConfig = { - targetType: 'mcpServer', + // Handle MCP server targets (existing endpoint, no code generation) + if (cliOptions.type === 'mcpServer' && cliOptions.endpoint) { + const config: McpServerTargetConfig = { + targetType: 'mcpServer', + name: cliOptions.name!, + description: cliOptions.description ?? `Tool for ${cliOptions.name!}`, + endpoint: cliOptions.endpoint, + gateway: cliOptions.gateway!, + toolDefinition: { name: cliOptions.name!, description: cliOptions.description ?? `Tool for ${cliOptions.name!}`, - endpoint: cliOptions.endpoint, - gateway: cliOptions.gateway!, - toolDefinition: { - name: cliOptions.name!, - description: cliOptions.description ?? `Tool for ${cliOptions.name!}`, - inputSchema: { type: 'object' }, - }, - ...(cliOptions.outboundAuthType - ? { - outboundAuth: { - type: outboundAuthMap[cliOptions.outboundAuthType.toLowerCase()] ?? 'NONE', - credentialName: cliOptions.credentialName, - }, - } - : {}), - }; - const result = await this.createExternalGatewayTarget(config); - const output = { - success: true, - toolName: result.toolName, - sourcePath: result.projectPath || undefined, - }; - if (cliOptions.json) { - console.log(JSON.stringify(output)); - } else { - console.log(`Added gateway target '${result.toolName}'`); - } - return { target_type: telemetryTargetType, host: telemetryHost, outbound_auth: telemetryOutboundAuth }; - } - - const result = await this.add({ - name: cliOptions.name!, - description: cliOptions.description, - language: cliOptions.language ?? 'Python', - gateway: cliOptions.gateway, - host: cliOptions.host, - }); - - if (!result.success) { - throw new Error(result.error); - } - + inputSchema: { type: 'object' }, + }, + ...(cliOptions.outboundAuthType + ? { + outboundAuth: { + type: outboundAuthMap[cliOptions.outboundAuthType.toLowerCase()] ?? 'NONE', + credentialName: cliOptions.credentialName, + }, + } + : {}), + }; + const result = await this.createExternalGatewayTarget(config); + const output = { + success: true, + toolName: result.toolName, + sourcePath: result.projectPath || undefined, + }; if (cliOptions.json) { - console.log(JSON.stringify(result)); + console.log(JSON.stringify(output)); } else { console.log(`Added gateway target '${result.toolName}'`); - if (result.sourcePath) { - console.log(`Tool code: ${result.sourcePath}`); - } } - return { target_type: telemetryTargetType, host: telemetryHost, outbound_auth: telemetryOutboundAuth }; + } + + const result = await this.add({ + name: cliOptions.name!, + description: cliOptions.description, + language: cliOptions.language ?? 'Python', + gateway: cliOptions.gateway, + host: cliOptions.host, }); - process.exit(0); - } catch (error) { + + if (!result.success) { + throw new Error(result.error); + } + if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); + console.log(JSON.stringify(result)); } else { - console.error(`Error: ${getErrorMessage(error)}`); + console.log(`Added gateway target '${result.toolName}'`); + if (result.sourcePath) { + console.log(`Tool code: ${result.sourcePath}`); + } } - process.exit(1); - } + + return { target_type: telemetryTargetType, host: telemetryHost, outbound_auth: telemetryOutboundAuth }; + }); }); removeCmd diff --git a/src/cli/primitives/MemoryPrimitive.tsx b/src/cli/primitives/MemoryPrimitive.tsx index 42dffd6a4..8f425d9eb 100644 --- a/src/cli/primitives/MemoryPrimitive.tsx +++ b/src/cli/primitives/MemoryPrimitive.tsx @@ -17,7 +17,7 @@ import { import { DEFAULT_DELIVERY_TYPE, validateAddMemoryOptions } from '../commands/add/validate'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; -import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { cliCommandRun } from '../telemetry/cli-command-run.js'; import { requireTTY } from '../tui/guards/tty'; import { DEFAULT_EVENT_EXPIRY } from '../tui/screens/memory/types'; import { BasePrimitive } from './BasePrimitive'; @@ -184,92 +184,81 @@ export class MemoryPrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } - - if (cliOptions.name || cliOptions.json) { - // CLI mode - const client = await TelemetryClientAccessor.get(); - await client.withCommandRun('add.memory', async () => { - const expiry = cliOptions.expiry ? parseInt(cliOptions.expiry, 10) : undefined; - const validation = validateAddMemoryOptions({ - name: cliOptions.name, - strategies: cliOptions.strategies, - expiry, - deliveryType: cliOptions.deliveryType, - dataStreamArn: cliOptions.dataStreamArn, - contentLevel: cliOptions.streamContentLevel, - streamDeliveryResources: cliOptions.streamDeliveryResources, - }); - - if (!validation.valid) { - throw new Error(validation.error); - } - - const result = await this.add({ - name: cliOptions.name!, - strategies: cliOptions.strategies, - expiry, - deliveryType: cliOptions.deliveryType, - dataStreamArn: cliOptions.dataStreamArn, - contentLevel: cliOptions.streamContentLevel, - streamDeliveryResources: cliOptions.streamDeliveryResources, - }); - - if (!result.success) { - throw new Error(result.error); - } - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else { - console.log(`Added memory '${result.memoryName}'`); - } - - const strategyList = (cliOptions.strategies ?? '') - .split(',') - .map(s => s.trim().toUpperCase()) - .filter(Boolean); - return { - strategy_count: strategyList.length, - strategy_semantic: strategyList.includes('SEMANTIC'), - strategy_summarization: strategyList.includes('SUMMARIZATION'), - strategy_user_preference: strategyList.includes('USER_PREFERENCE'), - strategy_episodic: strategyList.includes('EPISODIC'), - }; - }); - process.exit(0); - } else { - // TUI fallback — dynamic imports to avoid pulling ink (async) into registry - requireTTY(); - const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ - import('ink'), - import('react'), - import('../tui/screens/add/AddFlow'), - ]); - const { clear, unmount } = render( - React.createElement(AddFlow, { - isInteractive: false, - initialResource: 'memory', - onExit: () => { - clear(); - unmount(); - process.exit(0); - }, - }) - ); - } - } catch (error) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); - } else { - console.error(getErrorMessage(error)); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); process.exit(1); } + + if (cliOptions.name || cliOptions.json) { + // CLI mode + await cliCommandRun('add.memory', !!cliOptions.json, async () => { + const expiry = cliOptions.expiry ? parseInt(cliOptions.expiry, 10) : undefined; + const validation = validateAddMemoryOptions({ + name: cliOptions.name, + strategies: cliOptions.strategies, + expiry, + deliveryType: cliOptions.deliveryType, + dataStreamArn: cliOptions.dataStreamArn, + contentLevel: cliOptions.streamContentLevel, + streamDeliveryResources: cliOptions.streamDeliveryResources, + }); + + if (!validation.valid) { + throw new Error(validation.error); + } + + const result = await this.add({ + name: cliOptions.name!, + strategies: cliOptions.strategies, + expiry, + deliveryType: cliOptions.deliveryType, + dataStreamArn: cliOptions.dataStreamArn, + contentLevel: cliOptions.streamContentLevel, + streamDeliveryResources: cliOptions.streamDeliveryResources, + }); + + if (!result.success) { + throw new Error(result.error); + } + + if (cliOptions.json) { + console.log(JSON.stringify(result)); + } else { + console.log(`Added memory '${result.memoryName}'`); + } + + const strategyList = (cliOptions.strategies ?? '') + .split(',') + .map(s => s.trim().toUpperCase()) + .filter(Boolean); + return { + strategy_count: strategyList.length, + strategy_semantic: strategyList.includes('SEMANTIC'), + strategy_summarization: strategyList.includes('SUMMARIZATION'), + strategy_user_preference: strategyList.includes('USER_PREFERENCE'), + strategy_episodic: strategyList.includes('EPISODIC'), + }; + }); + } else { + // TUI fallback — dynamic imports to avoid pulling ink (async) into registry + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + initialResource: 'memory', + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + } } ); diff --git a/src/cli/primitives/OnlineEvalConfigPrimitive.ts b/src/cli/primitives/OnlineEvalConfigPrimitive.ts index 44919aa71..3af51ff84 100644 --- a/src/cli/primitives/OnlineEvalConfigPrimitive.ts +++ b/src/cli/primitives/OnlineEvalConfigPrimitive.ts @@ -3,7 +3,7 @@ import type { OnlineEvalConfig } from '../../schema'; import { OnlineEvalConfigSchema } from '../../schema'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; -import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { cliCommandRun } from '../telemetry/cli-command-run.js'; import { requireTTY } from '../tui/guards/tty'; import { BasePrimitive } from './BasePrimitive'; import type { AddResult, AddScreenComponent, RemovableResource } from './types'; @@ -122,84 +122,73 @@ export class OnlineEvalConfigPrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } - - if (cliOptions.name || cliOptions.json) { - // Merge --evaluator and --evaluator-arn into a single list - const allEvaluators = [...(cliOptions.evaluator ?? []), ...(cliOptions.evaluatorArn ?? [])]; - - const client = await TelemetryClientAccessor.get(); - await client.withCommandRun('add.online-eval', async () => { - if (!cliOptions.name || !cliOptions.runtime || allEvaluators.length === 0 || !cliOptions.samplingRate) { - throw new Error( - '--name, --runtime, --evaluator (and/or --evaluator-arn), and --sampling-rate are all required in non-interactive mode' - ); - } - - // Sampling rate as a percentage of requests to evaluate (0.01% to 100%) - const samplingRate = parseFloat(cliOptions.samplingRate); - if (isNaN(samplingRate) || samplingRate < 0.01 || samplingRate > 100) { - throw new Error( - `Invalid --sampling-rate "${cliOptions.samplingRate}". Must be a percentage between 0.01 and 100` - ); - } - - const result = await this.add({ - name: cliOptions.name, - agent: cliOptions.runtime, - evaluators: allEvaluators, - samplingRate, - enableOnCreate: cliOptions.enableOnCreate, - }); - - if (!result.success) { - throw new Error(result.error); - } - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else { - console.log(`Added online eval config '${result.configName}'`); - } - - return { - evaluator_count: allEvaluators.length, - enable_on_create: cliOptions.enableOnCreate ?? false, - }; - }); - process.exit(0); - } else { - // TUI fallback - requireTTY(); - const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ - import('ink'), - import('react'), - import('../tui/screens/add/AddFlow'), - ]); - const { clear, unmount } = render( - React.createElement(AddFlow, { - isInteractive: false, - initialResource: 'online-eval', - onExit: () => { - clear(); - unmount(); - process.exit(0); - }, - }) - ); - } - } catch (error) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); - } else { - console.error(getErrorMessage(error)); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); process.exit(1); } + + if (cliOptions.name || cliOptions.json) { + // Merge --evaluator and --evaluator-arn into a single list + const allEvaluators = [...(cliOptions.evaluator ?? []), ...(cliOptions.evaluatorArn ?? [])]; + + await cliCommandRun('add.online-eval', !!cliOptions.json, async () => { + if (!cliOptions.name || !cliOptions.runtime || allEvaluators.length === 0 || !cliOptions.samplingRate) { + throw new Error( + '--name, --runtime, --evaluator (and/or --evaluator-arn), and --sampling-rate are all required in non-interactive mode' + ); + } + + // Sampling rate as a percentage of requests to evaluate (0.01% to 100%) + const samplingRate = parseFloat(cliOptions.samplingRate); + if (isNaN(samplingRate) || samplingRate < 0.01 || samplingRate > 100) { + throw new Error( + `Invalid --sampling-rate "${cliOptions.samplingRate}". Must be a percentage between 0.01 and 100` + ); + } + + const result = await this.add({ + name: cliOptions.name, + agent: cliOptions.runtime, + evaluators: allEvaluators, + samplingRate, + enableOnCreate: cliOptions.enableOnCreate, + }); + + if (!result.success) { + throw new Error(result.error); + } + + if (cliOptions.json) { + console.log(JSON.stringify(result)); + } else { + console.log(`Added online eval config '${result.configName}'`); + } + + return { + evaluator_count: allEvaluators.length, + enable_on_create: cliOptions.enableOnCreate ?? false, + }; + }); + } else { + // TUI fallback + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + initialResource: 'online-eval', + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + } } ); diff --git a/src/cli/primitives/PolicyEnginePrimitive.ts b/src/cli/primitives/PolicyEnginePrimitive.ts index 6efd141cc..a8d870b7c 100644 --- a/src/cli/primitives/PolicyEnginePrimitive.ts +++ b/src/cli/primitives/PolicyEnginePrimitive.ts @@ -3,7 +3,7 @@ import type { AgentCoreProjectSpec, PolicyEngine } from '../../schema'; import { PolicyEngineModeSchema, PolicyEngineSchema } from '../../schema'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; -import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { cliCommandRun } from '../telemetry/cli-command-run.js'; import { AttachMode, standardize } from '../telemetry/schemas/common-shapes.js'; import { requireTTY } from '../tui/guards/tty'; import { BasePrimitive } from './BasePrimitive'; @@ -223,82 +223,71 @@ export class PolicyEnginePrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } + + if (cliOptions.name || cliOptions.description || cliOptions.encryptionKeyArn || cliOptions.json) { + await cliCommandRun('add.policy-engine', !!cliOptions.json, async () => { + if (!cliOptions.name) { + throw new Error('--name is required'); + } + + const result = await this.add({ + name: cliOptions.name, + description: cliOptions.description, + encryptionKeyArn: cliOptions.encryptionKeyArn, + }); - if (cliOptions.name || cliOptions.description || cliOptions.encryptionKeyArn || cliOptions.json) { - const client = await TelemetryClientAccessor.get(); - await client.withCommandRun('add.policy-engine', async () => { - if (!cliOptions.name) { - throw new Error('--name is required'); - } - - const result = await this.add({ - name: cliOptions.name, - description: cliOptions.description, - encryptionKeyArn: cliOptions.encryptionKeyArn, - }); - - // Attach to gateways if requested - if (result.success && cliOptions.attachToGateways) { - const mode = PolicyEngineModeSchema.parse(cliOptions.attachMode ?? 'LOG_ONLY'); - const gateways = cliOptions.attachToGateways + // Attach to gateways if requested + if (result.success && cliOptions.attachToGateways) { + const mode = PolicyEngineModeSchema.parse(cliOptions.attachMode ?? 'LOG_ONLY'); + const gateways = cliOptions.attachToGateways + .split(',') + .map(s => s.trim()) + .filter(Boolean); + await this.attachToGateways(cliOptions.name, gateways, mode); + } + + if (!result.success) { + throw new Error(result.error); + } + + if (cliOptions.json) { + console.log(JSON.stringify(result)); + } else { + console.log(`Added policy engine '${result.engineName}'`); + } + + const gatewayCount = cliOptions.attachToGateways + ? cliOptions.attachToGateways .split(',') .map(s => s.trim()) - .filter(Boolean); - await this.attachToGateways(cliOptions.name, gateways, mode); - } - - if (!result.success) { - throw new Error(result.error); - } - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else { - console.log(`Added policy engine '${result.engineName}'`); - } - - const gatewayCount = cliOptions.attachToGateways - ? cliOptions.attachToGateways - .split(',') - .map(s => s.trim()) - .filter(Boolean).length - : 0; - return { - attach_gateway_count: gatewayCount, - attach_mode: standardize(AttachMode, cliOptions.attachMode ?? 'log_only'), - }; - }); - process.exit(0); - } else { - requireTTY(); - const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ - import('ink'), - import('react'), - import('../tui/screens/add/AddFlow'), - ]); - const { clear, unmount } = render( - React.createElement(AddFlow, { - isInteractive: false, - onExit: () => { - clear(); - unmount(); - process.exit(0); - }, - }) - ); - } - } catch (error) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); - } else { - console.error(`Error: ${getErrorMessage(error)}`); - } - process.exit(1); + .filter(Boolean).length + : 0; + return { + attach_gateway_count: gatewayCount, + attach_mode: standardize(AttachMode, cliOptions.attachMode ?? 'log_only'), + }; + }); + } else { + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); } } ); diff --git a/src/cli/primitives/PolicyPrimitive.ts b/src/cli/primitives/PolicyPrimitive.ts index 202585983..e129306bf 100644 --- a/src/cli/primitives/PolicyPrimitive.ts +++ b/src/cli/primitives/PolicyPrimitive.ts @@ -5,7 +5,7 @@ import { detectRegion } from '../aws'; import { getPolicyGeneration, startPolicyGeneration } from '../aws/policy-generation'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; -import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { cliCommandRun } from '../telemetry/cli-command-run.js'; import { ValidationMode, standardize } from '../telemetry/schemas/common-shapes.js'; import { requireTTY } from '../tui/guards/tty'; import { BasePrimitive } from './BasePrimitive'; @@ -293,89 +293,78 @@ export class PolicyPrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } - if ( - cliOptions.name || - cliOptions.engine || - cliOptions.source || - cliOptions.statement || - cliOptions.generate || - cliOptions.json - ) { - const client = await TelemetryClientAccessor.get(); - await client.withCommandRun('add.policy', async () => { - if (!cliOptions.name) { - throw new Error('--name is required'); - } - if (!cliOptions.engine) { - throw new Error('--engine is required'); - } - - const result = await this.add({ - name: cliOptions.name, - engine: cliOptions.engine, - description: cliOptions.description, - source: cliOptions.source, - statement: cliOptions.statement, - generate: cliOptions.generate, - gateway: cliOptions.gateway, - validationMode: cliOptions.validationMode - ? ValidationModeSchema.parse(cliOptions.validationMode) - : undefined, - }); - - if (!result.success) { - throw new Error(result.error); - } - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else { - console.log(`Added policy '${result.policyName}' to engine '${result.engineName}'`); - } - - const sourceType: 'file' | 'statement' | 'generate' = cliOptions.source - ? 'file' - : cliOptions.generate - ? 'generate' - : 'statement'; - return { - source_type: sourceType, - validation_mode: standardize(ValidationMode, cliOptions.validationMode ?? 'FAIL_ON_ANY_FINDINGS'), - }; + if ( + cliOptions.name || + cliOptions.engine || + cliOptions.source || + cliOptions.statement || + cliOptions.generate || + cliOptions.json + ) { + await cliCommandRun('add.policy', !!cliOptions.json, async () => { + if (!cliOptions.name) { + throw new Error('--name is required'); + } + if (!cliOptions.engine) { + throw new Error('--engine is required'); + } + + const result = await this.add({ + name: cliOptions.name, + engine: cliOptions.engine, + description: cliOptions.description, + source: cliOptions.source, + statement: cliOptions.statement, + generate: cliOptions.generate, + gateway: cliOptions.gateway, + validationMode: cliOptions.validationMode + ? ValidationModeSchema.parse(cliOptions.validationMode) + : undefined, }); - process.exit(0); - } else { - requireTTY(); - const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ - import('ink'), - import('react'), - import('../tui/screens/add/AddFlow'), - ]); - const { clear, unmount } = render( - React.createElement(AddFlow, { - isInteractive: false, - initialResource: 'policy', - onExit: () => { - clear(); - unmount(); - process.exit(0); - }, - }) - ); - } - } catch (error) { - if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); - } else { - console.error(`Error: ${getErrorMessage(error)}`); - } - process.exit(1); + + if (!result.success) { + throw new Error(result.error); + } + + if (cliOptions.json) { + console.log(JSON.stringify(result)); + } else { + console.log(`Added policy '${result.policyName}' to engine '${result.engineName}'`); + } + + const sourceType: 'file' | 'statement' | 'generate' = cliOptions.source + ? 'file' + : cliOptions.generate + ? 'generate' + : 'statement'; + return { + source_type: sourceType, + validation_mode: standardize(ValidationMode, cliOptions.validationMode ?? 'FAIL_ON_ANY_FINDINGS'), + }; + }); + } else { + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + initialResource: 'policy', + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); } } ); diff --git a/src/cli/primitives/RuntimeEndpointPrimitive.ts b/src/cli/primitives/RuntimeEndpointPrimitive.ts index aef1aecb6..e055fd17a 100644 --- a/src/cli/primitives/RuntimeEndpointPrimitive.ts +++ b/src/cli/primitives/RuntimeEndpointPrimitive.ts @@ -4,7 +4,7 @@ import { RuntimeEndpointSchema } from '../../schema'; import type { ResourceType } from '../commands/remove/types'; import { getErrorMessage } from '../errors'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; -import { TelemetryClientAccessor } from '../telemetry/client-accessor.js'; +import { cliCommandRun } from '../telemetry/cli-command-run.js'; import { BasePrimitive } from './BasePrimitive'; import { SOURCE_CODE_NOTE } from './constants'; import type { AddResult, AddScreenComponent, RemovableResource } from './types'; @@ -249,42 +249,31 @@ export class RuntimeEndpointPrimitive extends BasePrimitive { - try { - if (!findConfigRoot()) { - console.error('No agentcore project found. Run `agentcore create` first.'); - process.exit(1); - } + if (!findConfigRoot()) { + console.error('No agentcore project found. Run `agentcore create` first.'); + process.exit(1); + } - const client = await TelemetryClientAccessor.get(); - await client.withCommandRun('add.runtime-endpoint', async () => { - const result = await this.add({ - runtime: cliOptions.runtime, - endpoint: cliOptions.endpoint, - version: cliOptions.version, - description: cliOptions.description, - }); - - if (!result.success) { - throw new Error(result.error); - } - - if (cliOptions.json) { - console.log(JSON.stringify(result)); - } else { - console.log(`Added runtime endpoint '${cliOptions.endpoint}' to runtime '${cliOptions.runtime}'`); - } - - return {}; + await cliCommandRun('add.runtime-endpoint', !!cliOptions.json, async () => { + const result = await this.add({ + runtime: cliOptions.runtime, + endpoint: cliOptions.endpoint, + version: cliOptions.version, + description: cliOptions.description, }); - process.exit(0); - } catch (error) { + + if (!result.success) { + throw new Error(result.error); + } + if (cliOptions.json) { - console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); + console.log(JSON.stringify(result)); } else { - console.error(`Error: ${getErrorMessage(error)}`); + console.log(`Added runtime endpoint '${cliOptions.endpoint}' to runtime '${cliOptions.runtime}'`); } - process.exit(1); - } + + return {}; + }); } ); diff --git a/src/cli/telemetry/cli-command-run.ts b/src/cli/telemetry/cli-command-run.ts new file mode 100644 index 000000000..1e3ec1959 --- /dev/null +++ b/src/cli/telemetry/cli-command-run.ts @@ -0,0 +1,26 @@ +import { getErrorMessage } from '../errors'; +import { TelemetryClientAccessor } from './client-accessor.js'; +import type { Command, CommandAttrs } from './schemas/command-run.js'; + +/** + * Run a CLI command with telemetry, standardized error output, and process.exit. + * The callback should throw on failure and return telemetry attrs on success. + */ +export async function cliCommandRun( + command: C, + json: boolean, + fn: () => Promise> +): Promise { + try { + const client = await TelemetryClientAccessor.get(); + await client.withCommandRun(command, fn); + process.exit(0); + } catch (error) { + if (json) { + console.log(JSON.stringify({ success: false, error: getErrorMessage(error) })); + } else { + console.error(getErrorMessage(error)); + } + process.exit(1); + } +} From b102b94168b22cbd4cea30bf4d4f01147aca155d Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Wed, 29 Apr 2026 21:51:53 +0000 Subject: [PATCH 30/45] test: add audit file assertions for all add.* telemetry --- integ-tests/telemetry.test.ts | 147 +++++++++++++++++++++++++++++++++- 1 file changed, 145 insertions(+), 2 deletions(-) diff --git a/integ-tests/telemetry.test.ts b/integ-tests/telemetry.test.ts index 5347af0c8..2746b0680 100644 --- a/integ-tests/telemetry.test.ts +++ b/integ-tests/telemetry.test.ts @@ -1,9 +1,12 @@ import { spawnAndCollect } from '../src/test-utils/cli-runner.js'; -import { mkdtempSync } from 'node:fs'; +import { createTestProject } from '../src/test-utils/index.js'; +import type { TestProject } from '../src/test-utils/index.js'; +import { globSync } from 'glob'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; import { rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterAll, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; const testConfigDir = mkdtempSync(join(tmpdir(), 'agentcore-integ-')); const cliPath = join(__dirname, '..', 'dist', 'cli', 'index.mjs'); @@ -30,3 +33,143 @@ describe('telemetry e2e', () => { expect(status.stdout).toContain('global config'); }); }); + +// ── Audit file tests ─────────────────────────────────────────────────────── + +interface TelemetryEntry { + value: number; + attrs: Record; +} + +function readAuditEntries(configDir: string): TelemetryEntry[] { + const files = globSync(join(configDir, 'telemetry', '*.json')); + return files.flatMap(f => + readFileSync(f, 'utf-8') + .trim() + .split('\n') + .map(line => JSON.parse(line) as TelemetryEntry) + ); +} + +function clearAudit(configDir: string) { + try { + rmSync(join(configDir, 'telemetry'), { recursive: true, force: true }); + } catch { + /* ignore */ + } +} + +describe('telemetry audit: add.* commands', () => { + let project: TestProject; + const auditConfigDir = mkdtempSync(join(tmpdir(), 'agentcore-audit-')); + + function runWithAudit(args: string[]) { + return spawnAndCollect('node', [cliPath, ...args], project.projectPath, { + AGENTCORE_SKIP_INSTALL: '1', + AGENTCORE_TELEMETRY_AUDIT: '1', + AGENTCORE_CONFIG_DIR: auditConfigDir, + }); + } + + beforeAll(async () => { + project = await createTestProject({ noAgent: true }); + }); + + afterAll(async () => { + await project.cleanup(); + await rm(auditConfigDir, { recursive: true, force: true }); + }); + + beforeEach(() => clearAudit(auditConfigDir)); + + it('add.gateway emits correct telemetry', async () => { + const result = await runWithAudit(['add', 'gateway', '--name', 'testgw', '--authorizer-type', 'NONE', '--json']); + expect(result.exitCode).toBe(0); + + const entries = readAuditEntries(auditConfigDir); + expect(entries).toHaveLength(1); + expect(entries[0]!.attrs).toMatchObject({ + command: 'add.gateway', + command_group: 'add', + exit_reason: 'success', + authorizer_type: 'none', + has_policy_engine: 'false', + semantic_search: 'true', + runtime_count: 0, + }); + }); + + it('add.credential emits correct telemetry', async () => { + const result = await runWithAudit([ + 'add', + 'credential', + '--name', + 'testcred', + '--type', + 'api-key', + '--api-key', + 'secret123', + '--json', + ]); + expect(result.exitCode).toBe(0); + + const entries = readAuditEntries(auditConfigDir); + expect(entries).toHaveLength(1); + expect(entries[0]!.attrs).toMatchObject({ + command: 'add.credential', + exit_reason: 'success', + credential_type: 'api-key', + }); + }); + + it('add.memory emits correct telemetry', async () => { + const result = await runWithAudit([ + 'add', + 'memory', + '--name', + 'testmem', + '--strategies', + 'SEMANTIC,SUMMARIZATION', + '--expiry', + '30', + '--json', + ]); + expect(result.exitCode).toBe(0); + + const entries = readAuditEntries(auditConfigDir); + expect(entries).toHaveLength(1); + expect(entries[0]!.attrs).toMatchObject({ + command: 'add.memory', + exit_reason: 'success', + strategy_count: 2, + strategy_semantic: 'true', + strategy_summarization: 'true', + }); + }); + + it('add.policy-engine emits correct telemetry', async () => { + const result = await runWithAudit(['add', 'policy-engine', '--name', 'testengine', '--json']); + expect(result.exitCode).toBe(0); + + const entries = readAuditEntries(auditConfigDir); + expect(entries).toHaveLength(1); + expect(entries[0]!.attrs).toMatchObject({ + command: 'add.policy-engine', + exit_reason: 'success', + attach_gateway_count: 0, + attach_mode: 'log_only', + }); + }); + + it('records failure telemetry when validation fails', async () => { + const result = await runWithAudit(['add', 'gateway', '--json']); + expect(result.exitCode).toBe(1); + + const entries = readAuditEntries(auditConfigDir); + expect(entries).toHaveLength(1); + expect(entries[0]!.attrs).toMatchObject({ + command: 'add.gateway', + exit_reason: 'failure', + }); + }); +}); From 72ab9d3bcca84d554c8c85f9105796ac82686583 Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Wed, 29 Apr 2026 21:58:01 +0000 Subject: [PATCH 31/45] test: add telemetry audit assertions to existing add integ tests --- e2e-tests/byo-custom-jwt.test.ts | 2 +- integ-tests/add-remove-resources.test.ts | 41 ++++++- integ-tests/create-no-agent.test.ts | 2 +- integ-tests/create-with-agent.test.ts | 2 +- integ-tests/dev-server.test.ts | 2 +- integ-tests/telemetry.test.ts | 147 +---------------------- src/test-utils/cli-runner.ts | 12 +- src/test-utils/project-factory.ts | 2 +- 8 files changed, 56 insertions(+), 154 deletions(-) diff --git a/e2e-tests/byo-custom-jwt.test.ts b/e2e-tests/byo-custom-jwt.test.ts index b7391a522..64e534e20 100644 --- a/e2e-tests/byo-custom-jwt.test.ts +++ b/e2e-tests/byo-custom-jwt.test.ts @@ -48,7 +48,7 @@ const region = process.env.AWS_REGION ?? 'us-east-1'; * Run the local CLI build without skipping install (needed for deploy). */ function runLocalCLI(args: string[], cwd: string): Promise { - return runCLI(args, cwd, /* skipInstall */ false); + return runCLI(args, cwd, { skipInstall: false }); } describe.sequential('e2e: BYO agent with CUSTOM_JWT auth', () => { diff --git a/integ-tests/add-remove-resources.test.ts b/integ-tests/add-remove-resources.test.ts index 57dd48483..154e00de1 100644 --- a/integ-tests/add-remove-resources.test.ts +++ b/integ-tests/add-remove-resources.test.ts @@ -1,7 +1,28 @@ import { createTestProject, readProjectConfig, runCLI } from '../src/test-utils/index.js'; import type { TestProject } from '../src/test-utils/index.js'; +import { globSync } from 'glob'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +const auditDir = mkdtempSync(join(tmpdir(), 'agentcore-audit-')); +const auditEnv = { AGENTCORE_TELEMETRY_AUDIT: '1', AGENTCORE_CONFIG_DIR: auditDir }; + +interface TelemetryEntry { + value: number; + attrs: Record; +} + +function readAuditEntries(): TelemetryEntry[] { + return globSync(join(auditDir, 'telemetry', '*.json')).flatMap(f => + readFileSync(f, 'utf-8') + .trim() + .split('\n') + .map(line => JSON.parse(line) as TelemetryEntry) + ); +} + describe('integration: add and remove resources', () => { let project: TestProject; @@ -16,13 +37,16 @@ describe('integration: add and remove resources', () => { afterAll(async () => { await project.cleanup(); + rmSync(auditDir, { recursive: true, force: true }); }); describe('memory lifecycle', () => { const memoryName = `IntegMem${Date.now().toString().slice(-6)}`; it('adds a memory resource', async () => { - const result = await runCLI(['add', 'memory', '--name', memoryName, '--json'], project.projectPath); + const result = await runCLI(['add', 'memory', '--name', memoryName, '--json'], project.projectPath, { + env: auditEnv, + }); expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); const json = JSON.parse(result.stdout); @@ -34,6 +58,12 @@ describe('integration: add and remove resources', () => { expect(memories, 'memories should exist').toBeDefined(); const found = memories!.some((m: Record) => m.name === memoryName); expect(found, `Memory "${memoryName}" should be in config`).toBe(true); + + // Verify telemetry + const entries = readAuditEntries(); + expect(entries.length).toBeGreaterThan(0); + const last = entries[entries.length - 1]!.attrs; + expect(last).toMatchObject({ command: 'add.memory', exit_reason: 'success' }); }); it('adds a memory with EPISODIC strategy and verifies reflectionNamespaces', async () => { @@ -86,7 +116,8 @@ describe('integration: add and remove resources', () => { it('adds a credential resource', async () => { const result = await runCLI( ['add', 'credential', '--name', credentialName, '--api-key', 'test-key-integ-123', '--json'], - project.projectPath + project.projectPath, + { env: auditEnv } ); expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); @@ -99,6 +130,12 @@ describe('integration: add and remove resources', () => { expect(credentials, 'credentials should exist').toBeDefined(); const found = credentials!.some((c: Record) => c.name === credentialName); expect(found, `Credential "${credentialName}" should be in config`).toBe(true); + + // Verify telemetry + const entries = readAuditEntries(); + expect(entries.length).toBeGreaterThan(0); + const last = entries[entries.length - 1]!.attrs; + expect(last).toMatchObject({ command: 'add.credential', exit_reason: 'success', credential_type: 'api-key' }); }); it('removes the credential resource', async () => { diff --git a/integ-tests/create-no-agent.test.ts b/integ-tests/create-no-agent.test.ts index 4bcca2690..bcdf80eaa 100644 --- a/integ-tests/create-no-agent.test.ts +++ b/integ-tests/create-no-agent.test.ts @@ -32,7 +32,7 @@ describe('integration: create without agent', () => { it.skipIf(!hasNpm || !hasGit)('creates project with real npm install and git init', async () => { const name = `NoAgent${Date.now().toString().slice(-6)}`; - const result = await runCLI(['create', '--name', name, '--no-agent', '--json'], testDir, false); + const result = await runCLI(['create', '--name', name, '--no-agent', '--json'], testDir, { skipInstall: false }); expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); diff --git a/integ-tests/create-with-agent.test.ts b/integ-tests/create-with-agent.test.ts index 7fb20bdbf..69f0594b8 100644 --- a/integ-tests/create-with-agent.test.ts +++ b/integ-tests/create-with-agent.test.ts @@ -49,7 +49,7 @@ describe('integration: create with Python agent', () => { '--json', ], testDir, - false + { skipInstall: false } ); expect(result.exitCode, `stderr: ${result.stderr}`).toBe(0); diff --git a/integ-tests/dev-server.test.ts b/integ-tests/dev-server.test.ts index 5f60976e7..4b07b7284 100644 --- a/integ-tests/dev-server.test.ts +++ b/integ-tests/dev-server.test.ts @@ -60,7 +60,7 @@ describe('integration: dev server', () => { '--json', ], testDir, - false + { skipInstall: false } ); if (result.exitCode === 0) { diff --git a/integ-tests/telemetry.test.ts b/integ-tests/telemetry.test.ts index 2746b0680..5347af0c8 100644 --- a/integ-tests/telemetry.test.ts +++ b/integ-tests/telemetry.test.ts @@ -1,12 +1,9 @@ import { spawnAndCollect } from '../src/test-utils/cli-runner.js'; -import { createTestProject } from '../src/test-utils/index.js'; -import type { TestProject } from '../src/test-utils/index.js'; -import { globSync } from 'glob'; -import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { mkdtempSync } from 'node:fs'; import { rm } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { afterAll, describe, expect, it } from 'vitest'; const testConfigDir = mkdtempSync(join(tmpdir(), 'agentcore-integ-')); const cliPath = join(__dirname, '..', 'dist', 'cli', 'index.mjs'); @@ -33,143 +30,3 @@ describe('telemetry e2e', () => { expect(status.stdout).toContain('global config'); }); }); - -// ── Audit file tests ─────────────────────────────────────────────────────── - -interface TelemetryEntry { - value: number; - attrs: Record; -} - -function readAuditEntries(configDir: string): TelemetryEntry[] { - const files = globSync(join(configDir, 'telemetry', '*.json')); - return files.flatMap(f => - readFileSync(f, 'utf-8') - .trim() - .split('\n') - .map(line => JSON.parse(line) as TelemetryEntry) - ); -} - -function clearAudit(configDir: string) { - try { - rmSync(join(configDir, 'telemetry'), { recursive: true, force: true }); - } catch { - /* ignore */ - } -} - -describe('telemetry audit: add.* commands', () => { - let project: TestProject; - const auditConfigDir = mkdtempSync(join(tmpdir(), 'agentcore-audit-')); - - function runWithAudit(args: string[]) { - return spawnAndCollect('node', [cliPath, ...args], project.projectPath, { - AGENTCORE_SKIP_INSTALL: '1', - AGENTCORE_TELEMETRY_AUDIT: '1', - AGENTCORE_CONFIG_DIR: auditConfigDir, - }); - } - - beforeAll(async () => { - project = await createTestProject({ noAgent: true }); - }); - - afterAll(async () => { - await project.cleanup(); - await rm(auditConfigDir, { recursive: true, force: true }); - }); - - beforeEach(() => clearAudit(auditConfigDir)); - - it('add.gateway emits correct telemetry', async () => { - const result = await runWithAudit(['add', 'gateway', '--name', 'testgw', '--authorizer-type', 'NONE', '--json']); - expect(result.exitCode).toBe(0); - - const entries = readAuditEntries(auditConfigDir); - expect(entries).toHaveLength(1); - expect(entries[0]!.attrs).toMatchObject({ - command: 'add.gateway', - command_group: 'add', - exit_reason: 'success', - authorizer_type: 'none', - has_policy_engine: 'false', - semantic_search: 'true', - runtime_count: 0, - }); - }); - - it('add.credential emits correct telemetry', async () => { - const result = await runWithAudit([ - 'add', - 'credential', - '--name', - 'testcred', - '--type', - 'api-key', - '--api-key', - 'secret123', - '--json', - ]); - expect(result.exitCode).toBe(0); - - const entries = readAuditEntries(auditConfigDir); - expect(entries).toHaveLength(1); - expect(entries[0]!.attrs).toMatchObject({ - command: 'add.credential', - exit_reason: 'success', - credential_type: 'api-key', - }); - }); - - it('add.memory emits correct telemetry', async () => { - const result = await runWithAudit([ - 'add', - 'memory', - '--name', - 'testmem', - '--strategies', - 'SEMANTIC,SUMMARIZATION', - '--expiry', - '30', - '--json', - ]); - expect(result.exitCode).toBe(0); - - const entries = readAuditEntries(auditConfigDir); - expect(entries).toHaveLength(1); - expect(entries[0]!.attrs).toMatchObject({ - command: 'add.memory', - exit_reason: 'success', - strategy_count: 2, - strategy_semantic: 'true', - strategy_summarization: 'true', - }); - }); - - it('add.policy-engine emits correct telemetry', async () => { - const result = await runWithAudit(['add', 'policy-engine', '--name', 'testengine', '--json']); - expect(result.exitCode).toBe(0); - - const entries = readAuditEntries(auditConfigDir); - expect(entries).toHaveLength(1); - expect(entries[0]!.attrs).toMatchObject({ - command: 'add.policy-engine', - exit_reason: 'success', - attach_gateway_count: 0, - attach_mode: 'log_only', - }); - }); - - it('records failure telemetry when validation fails', async () => { - const result = await runWithAudit(['add', 'gateway', '--json']); - expect(result.exitCode).toBe(1); - - const entries = readAuditEntries(auditConfigDir); - expect(entries).toHaveLength(1); - expect(entries[0]!.attrs).toMatchObject({ - command: 'add.gateway', - exit_reason: 'failure', - }); - }); -}); diff --git a/src/test-utils/cli-runner.ts b/src/test-utils/cli-runner.ts index 789624364..4526546c5 100644 --- a/src/test-utils/cli-runner.ts +++ b/src/test-utils/cli-runner.ts @@ -72,6 +72,14 @@ function getCLIPath(): string { * Run the AgentCore CLI via the local build (unit/integ tests). * Skips dependency installation by default for speed. */ -export async function runCLI(args: string[], cwd: string, skipInstall = true): Promise { - return spawnAndCollect('node', [getCLIPath(), ...args], cwd, skipInstall ? { AGENTCORE_SKIP_INSTALL: '1' } : {}); +export async function runCLI( + args: string[], + cwd: string, + options: { skipInstall?: boolean; env?: Record } = {} +): Promise { + const { skipInstall = true, env } = options; + return spawnAndCollect('node', [getCLIPath(), ...args], cwd, { + ...(skipInstall ? { AGENTCORE_SKIP_INSTALL: '1' } : {}), + ...env, + }); } diff --git a/src/test-utils/project-factory.ts b/src/test-utils/project-factory.ts index 77c77e1c6..61525f6c5 100644 --- a/src/test-utils/project-factory.ts +++ b/src/test-utils/project-factory.ts @@ -65,7 +65,7 @@ export async function createTestProject(options: CreateTestProjectOptions = {}): args.push('--json'); - const result = await runCLI(args, testDir, skipInstall); + const result = await runCLI(args, testDir, { skipInstall }); if (result.exitCode !== 0) { // Clean up on failure From e521dbccd94f8756bb10398852018960fecabad5 Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Wed, 29 Apr 2026 22:05:47 +0000 Subject: [PATCH 32/45] refactor: extract shared audit test utils into src/test-utils/audit.ts --- integ-tests/add-remove-resources.test.ts | 48 +++++++++--------------- integ-tests/help.test.ts | 45 ++++++++++------------ src/test-utils/audit.ts | 44 ++++++++++++++++++++++ src/test-utils/index.ts | 1 + 4 files changed, 82 insertions(+), 56 deletions(-) create mode 100644 src/test-utils/audit.ts diff --git a/integ-tests/add-remove-resources.test.ts b/integ-tests/add-remove-resources.test.ts index 154e00de1..844708741 100644 --- a/integ-tests/add-remove-resources.test.ts +++ b/integ-tests/add-remove-resources.test.ts @@ -1,27 +1,9 @@ +import { createAuditContext } from '../src/test-utils/audit.js'; import { createTestProject, readProjectConfig, runCLI } from '../src/test-utils/index.js'; import type { TestProject } from '../src/test-utils/index.js'; -import { globSync } from 'glob'; -import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; -import { tmpdir } from 'node:os'; -import { join } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -const auditDir = mkdtempSync(join(tmpdir(), 'agentcore-audit-')); -const auditEnv = { AGENTCORE_TELEMETRY_AUDIT: '1', AGENTCORE_CONFIG_DIR: auditDir }; - -interface TelemetryEntry { - value: number; - attrs: Record; -} - -function readAuditEntries(): TelemetryEntry[] { - return globSync(join(auditDir, 'telemetry', '*.json')).flatMap(f => - readFileSync(f, 'utf-8') - .trim() - .split('\n') - .map(line => JSON.parse(line) as TelemetryEntry) - ); -} +const audit = createAuditContext(); describe('integration: add and remove resources', () => { let project: TestProject; @@ -37,7 +19,7 @@ describe('integration: add and remove resources', () => { afterAll(async () => { await project.cleanup(); - rmSync(auditDir, { recursive: true, force: true }); + audit.cleanup(); }); describe('memory lifecycle', () => { @@ -45,7 +27,7 @@ describe('integration: add and remove resources', () => { it('adds a memory resource', async () => { const result = await runCLI(['add', 'memory', '--name', memoryName, '--json'], project.projectPath, { - env: auditEnv, + env: audit.env, }); expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); @@ -60,10 +42,10 @@ describe('integration: add and remove resources', () => { expect(found, `Memory "${memoryName}" should be in config`).toBe(true); // Verify telemetry - const entries = readAuditEntries(); - expect(entries.length).toBeGreaterThan(0); - const last = entries[entries.length - 1]!.attrs; - expect(last).toMatchObject({ command: 'add.memory', exit_reason: 'success' }); + const entries = audit.readEntries(); + const memEntry = entries.find(e => e.attrs.command === 'add.memory'); + expect(memEntry).toBeDefined(); + expect(memEntry!.attrs).toMatchObject({ command: 'add.memory', exit_reason: 'success' }); }); it('adds a memory with EPISODIC strategy and verifies reflectionNamespaces', async () => { @@ -117,7 +99,7 @@ describe('integration: add and remove resources', () => { const result = await runCLI( ['add', 'credential', '--name', credentialName, '--api-key', 'test-key-integ-123', '--json'], project.projectPath, - { env: auditEnv } + { env: audit.env } ); expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); @@ -132,10 +114,14 @@ describe('integration: add and remove resources', () => { expect(found, `Credential "${credentialName}" should be in config`).toBe(true); // Verify telemetry - const entries = readAuditEntries(); - expect(entries.length).toBeGreaterThan(0); - const last = entries[entries.length - 1]!.attrs; - expect(last).toMatchObject({ command: 'add.credential', exit_reason: 'success', credential_type: 'api-key' }); + const entries = audit.readEntries(); + const credEntry = entries.find(e => e.attrs.command === 'add.credential'); + expect(credEntry).toBeDefined(); + expect(credEntry!.attrs).toMatchObject({ + command: 'add.credential', + exit_reason: 'success', + credential_type: 'api-key', + }); }); it('removes the credential resource', async () => { diff --git a/integ-tests/help.test.ts b/integ-tests/help.test.ts index 052605c7a..93aa447d3 100644 --- a/integ-tests/help.test.ts +++ b/integ-tests/help.test.ts @@ -1,10 +1,9 @@ +import { createAuditContext } from '../src/test-utils/audit.js'; import { spawnAndCollect } from '../src/test-utils/cli-runner.js'; import { runCLI } from '../src/test-utils/index.js'; import { readdirSync } from 'node:fs'; -import { mkdir, readFile, rm } from 'node:fs/promises'; -import { tmpdir } from 'node:os'; import { join } from 'node:path'; -import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import { afterAll, describe, expect, it } from 'vitest'; const COMMANDS = [ 'create', @@ -45,52 +44,48 @@ describe('CLI help', () => { }); describe('help modes telemetry', () => { - let testConfigDir: string; + const audit = createAuditContext(); const cliPath = join(__dirname, '..', 'dist', 'cli', 'index.mjs'); - beforeAll(async () => { - testConfigDir = join(tmpdir(), `agentcore-help-telemetry-${Date.now()}`); - await mkdir(testConfigDir, { recursive: true }); - }); - afterAll(() => rm(testConfigDir, { recursive: true, force: true })); + afterAll(() => audit.cleanup()); function run(args: string[], extraEnv: Record = {}) { - return spawnAndCollect('node', [cliPath, ...args], tmpdir(), { + return spawnAndCollect('node', [cliPath, ...args], process.cwd(), { AGENTCORE_SKIP_INSTALL: '1', - AGENTCORE_CONFIG_DIR: testConfigDir, + ...audit.env, ...extraEnv, }); } it('writes JSONL audit file when audit is enabled via env var', async () => { - const result = await run(['help', 'modes'], { AGENTCORE_TELEMETRY_AUDIT: '1' }); + const result = await run(['help', 'modes']); expect(result.exitCode).toBe(0); - const telemetryDir = join(testConfigDir, 'telemetry'); - const files = readdirSync(telemetryDir).filter(f => f.startsWith('help-')); - expect(files).toHaveLength(1); - - const content = await readFile(join(telemetryDir, files[0]!), 'utf-8'); - const entry = JSON.parse(content.trim()); - expect(entry.attrs).toMatchObject({ + const entries = audit.readEntries(); + expect(entries).toHaveLength(1); + expect(entries[0]!.attrs).toMatchObject({ 'service.name': 'agentcore-cli', 'agentcore-cli.mode': 'cli', command_group: 'help', command: 'help.modes', exit_reason: 'success', }); - expect(entry.attrs['agentcore-cli.session_id']).toBeDefined(); - expect(entry.attrs['os.type']).toBeDefined(); - expect(entry.value).toBeGreaterThanOrEqual(0); + expect(entries[0]!.attrs['agentcore-cli.session_id']).toBeDefined(); + expect(entries[0]!.attrs['os.type']).toBeDefined(); + expect(entries[0]!.value).toBeGreaterThanOrEqual(0); }); it('does not write audit file when audit is not enabled', async () => { - const telemetryDir = join(testConfigDir, 'telemetry'); - await rm(telemetryDir, { recursive: true, force: true }); + audit.clear(); - const result = await run(['help', 'modes']); + const noAuditCliPath = join(__dirname, '..', 'dist', 'cli', 'index.mjs'); + const result = await spawnAndCollect('node', [noAuditCliPath, 'help', 'modes'], process.cwd(), { + AGENTCORE_SKIP_INSTALL: '1', + AGENTCORE_CONFIG_DIR: audit.dir, + }); expect(result.exitCode).toBe(0); + const telemetryDir = join(audit.dir, 'telemetry'); try { const files = readdirSync(telemetryDir); expect(files).toHaveLength(0); diff --git a/src/test-utils/audit.ts b/src/test-utils/audit.ts new file mode 100644 index 000000000..857a00e05 --- /dev/null +++ b/src/test-utils/audit.ts @@ -0,0 +1,44 @@ +import { globSync } from 'glob'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +export interface TelemetryEntry { + value: number; + attrs: Record; +} + +export interface AuditContext { + /** Temp directory used as AGENTCORE_CONFIG_DIR */ + dir: string; + /** Env vars to pass to runCLI */ + env: { AGENTCORE_TELEMETRY_AUDIT: '1'; AGENTCORE_CONFIG_DIR: string }; + /** Read all JSONL entries from the audit telemetry directory */ + readEntries: () => TelemetryEntry[]; + /** Delete the telemetry subdirectory */ + clear: () => void; + /** Delete the entire config directory */ + cleanup: () => void; +} + +export function createAuditContext(): AuditContext { + const dir = mkdtempSync(join(tmpdir(), 'agentcore-audit-')); + return { + dir, + env: { AGENTCORE_TELEMETRY_AUDIT: '1', AGENTCORE_CONFIG_DIR: dir }, + readEntries() { + return globSync(join(dir, 'telemetry', '*.json')).flatMap(f => + readFileSync(f, 'utf-8') + .trim() + .split('\n') + .map(line => JSON.parse(line) as TelemetryEntry) + ); + }, + clear() { + rmSync(join(dir, 'telemetry'), { recursive: true, force: true }); + }, + cleanup() { + rmSync(dir, { recursive: true, force: true }); + }, + }; +} diff --git a/src/test-utils/index.ts b/src/test-utils/index.ts index ff127a35e..a9aad665d 100644 --- a/src/test-utils/index.ts +++ b/src/test-utils/index.ts @@ -4,6 +4,7 @@ */ export { runCLI, spawnAndCollect, cleanSpawnEnv, type RunResult } from './cli-runner.js'; +export { createAuditContext, type AuditContext, type TelemetryEntry } from './audit.js'; export { exists } from './fs-helpers.js'; export { hasCommand, hasAwsCredentials, prereqs } from './prereqs.js'; export { createTestProject, type TestProject, type CreateTestProjectOptions } from './project-factory.js'; From f152b0c67ac37676dd8eb718f2c66686e70d45a3 Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Wed, 29 Apr 2026 22:25:26 +0000 Subject: [PATCH 33/45] =?UTF-8?q?fix:=20address=20review=20feedback=20?= =?UTF-8?q?=E2=80=94=20guard=20telemetry=20init,=20replaceAll,=20unknown?= =?UTF-8?q?=20fallback,=20TUI=20try/catch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli/primitives/CredentialPrimitive.tsx | 41 +++++++++++-------- src/cli/primitives/EvaluatorPrimitive.ts | 41 +++++++++++-------- src/cli/primitives/GatewayTargetPrimitive.ts | 4 +- src/cli/primitives/MemoryPrimitive.tsx | 41 +++++++++++-------- .../primitives/OnlineEvalConfigPrimitive.ts | 41 +++++++++++-------- src/cli/primitives/PolicyEnginePrimitive.ts | 37 +++++++++-------- src/cli/primitives/PolicyPrimitive.ts | 39 ++++++++++-------- src/cli/telemetry/cli-command-run.ts | 13 +++++- src/cli/telemetry/schemas/common-shapes.ts | 1 + 9 files changed, 150 insertions(+), 108 deletions(-) diff --git a/src/cli/primitives/CredentialPrimitive.tsx b/src/cli/primitives/CredentialPrimitive.tsx index e9bf61e24..9607094f8 100644 --- a/src/cli/primitives/CredentialPrimitive.tsx +++ b/src/cli/primitives/CredentialPrimitive.tsx @@ -342,24 +342,29 @@ export class CredentialPrimitive extends BasePrimitive { - clear(); - unmount(); - process.exit(0); - }, - }) - ); + try { + // TUI fallback — dynamic imports to avoid pulling ink (async) into registry + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + initialResource: 'credential', + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } } } ); diff --git a/src/cli/primitives/EvaluatorPrimitive.ts b/src/cli/primitives/EvaluatorPrimitive.ts index f8a7d3fa2..45459fec7 100644 --- a/src/cli/primitives/EvaluatorPrimitive.ts +++ b/src/cli/primitives/EvaluatorPrimitive.ts @@ -320,24 +320,29 @@ export class EvaluatorPrimitive extends BasePrimitive { - clear(); - unmount(); - process.exit(0); - }, - }) - ); + try { + // TUI fallback + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + initialResource: 'evaluator', + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } } } ); diff --git a/src/cli/primitives/GatewayTargetPrimitive.ts b/src/cli/primitives/GatewayTargetPrimitive.ts index 2323a37a0..95d237e65 100644 --- a/src/cli/primitives/GatewayTargetPrimitive.ts +++ b/src/cli/primitives/GatewayTargetPrimitive.ts @@ -330,10 +330,10 @@ export class GatewayTargetPrimitive extends BasePrimitive { - clear(); - unmount(); - process.exit(0); - }, - }) - ); + try { + // TUI fallback — dynamic imports to avoid pulling ink (async) into registry + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + initialResource: 'memory', + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } } } ); diff --git a/src/cli/primitives/OnlineEvalConfigPrimitive.ts b/src/cli/primitives/OnlineEvalConfigPrimitive.ts index 3af51ff84..5a85e950a 100644 --- a/src/cli/primitives/OnlineEvalConfigPrimitive.ts +++ b/src/cli/primitives/OnlineEvalConfigPrimitive.ts @@ -170,24 +170,29 @@ export class OnlineEvalConfigPrimitive extends BasePrimitive { - clear(); - unmount(); - process.exit(0); - }, - }) - ); + try { + // TUI fallback + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + initialResource: 'online-eval', + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } } } ); diff --git a/src/cli/primitives/PolicyEnginePrimitive.ts b/src/cli/primitives/PolicyEnginePrimitive.ts index a8d870b7c..bb9b314d8 100644 --- a/src/cli/primitives/PolicyEnginePrimitive.ts +++ b/src/cli/primitives/PolicyEnginePrimitive.ts @@ -272,22 +272,27 @@ export class PolicyEnginePrimitive extends BasePrimitive { - clear(); - unmount(); - process.exit(0); - }, - }) - ); + try { + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } } } ); diff --git a/src/cli/primitives/PolicyPrimitive.ts b/src/cli/primitives/PolicyPrimitive.ts index e129306bf..beefe3008 100644 --- a/src/cli/primitives/PolicyPrimitive.ts +++ b/src/cli/primitives/PolicyPrimitive.ts @@ -348,23 +348,28 @@ export class PolicyPrimitive extends BasePrimitive { - clear(); - unmount(); - process.exit(0); - }, - }) - ); + try { + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + initialResource: 'policy', + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } } } ); diff --git a/src/cli/telemetry/cli-command-run.ts b/src/cli/telemetry/cli-command-run.ts index 1e3ec1959..4d3613e6e 100644 --- a/src/cli/telemetry/cli-command-run.ts +++ b/src/cli/telemetry/cli-command-run.ts @@ -5,6 +5,9 @@ import type { Command, CommandAttrs } from './schemas/command-run.js'; /** * Run a CLI command with telemetry, standardized error output, and process.exit. * The callback should throw on failure and return telemetry attrs on success. + * + * If telemetry initialization fails, the command still runs without telemetry — + * telemetry must never block CLI behavior. */ export async function cliCommandRun( command: C, @@ -12,7 +15,15 @@ export async function cliCommandRun( fn: () => Promise> ): Promise { try { - const client = await TelemetryClientAccessor.get(); + let client; + try { + client = await TelemetryClientAccessor.get(); + } catch { + // Telemetry init failed — run without it + await fn(); + process.exit(0); + } + // withCommandRun records success/failure telemetry, then re-throws on failure await client.withCommandRun(command, fn); process.exit(0); } catch (error) { diff --git a/src/cli/telemetry/schemas/common-shapes.ts b/src/cli/telemetry/schemas/common-shapes.ts index 3d52c28e3..80a3a06c7 100644 --- a/src/cli/telemetry/schemas/common-shapes.ts +++ b/src/cli/telemetry/schemas/common-shapes.ts @@ -65,6 +65,7 @@ export const GatewayTargetType = z.enum([ 'open-api-schema', 'smithy-model', 'lambda-function-arn', + 'unknown', ]); export const Language = z.enum(['python', 'typescript', 'other']); export const Level = z.enum(['session', 'trace', 'tool_call']); From f7003ca74dbab5cda583328256ee290266717e92 Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Wed, 29 Apr 2026 22:54:07 +0000 Subject: [PATCH 34/45] fix: AgentPrimitive TUI try/catch, standardize uses safeParse --- src/cli/primitives/AgentPrimitive.tsx | 41 ++++++++++++---------- src/cli/telemetry/schemas/common-shapes.ts | 6 +++- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/cli/primitives/AgentPrimitive.tsx b/src/cli/primitives/AgentPrimitive.tsx index 7f94e1595..3bd04d6ee 100644 --- a/src/cli/primitives/AgentPrimitive.tsx +++ b/src/cli/primitives/AgentPrimitive.tsx @@ -354,24 +354,29 @@ export class AgentPrimitive extends BasePrimitive { - clear(); - unmount(); - process.exit(0); - }, - }) - ); + try { + // TUI fallback — dynamic imports to avoid pulling ink (async) into registry + requireTTY(); + const [{ render }, { default: React }, { AddFlow }] = await Promise.all([ + import('ink'), + import('react'), + import('../tui/screens/add/AddFlow'), + ]); + const { clear, unmount } = render( + React.createElement(AddFlow, { + isInteractive: false, + initialResource: 'agent', + onExit: () => { + clear(); + unmount(); + process.exit(0); + }, + }) + ); + } catch (error) { + console.error(getErrorMessage(error)); + process.exit(1); + } } }); diff --git a/src/cli/telemetry/schemas/common-shapes.ts b/src/cli/telemetry/schemas/common-shapes.ts index 80a3a06c7..6ee451a10 100644 --- a/src/cli/telemetry/schemas/common-shapes.ts +++ b/src/cli/telemetry/schemas/common-shapes.ts @@ -29,7 +29,11 @@ export function resilientParse( /** Lowercase a CLI value and parse it through a Zod enum, returning the narrowed type. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function standardize>(schema: T, value: string): z.infer { - return schema.parse(value.toLowerCase()) as z.infer; + const lower = value.toLowerCase(); + const result = schema.safeParse(lower); + // If the value doesn't match the enum, return the lowercased value anyway — + // recordCommandRun's try/catch will silently drop the invalid metric. + return (result.success ? result.data : lower) as z.infer; } // Primitive types From 091264c3483f9743c76045bab5a23039821874cd Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Thu, 30 Apr 2026 00:25:49 +0000 Subject: [PATCH 35/45] refactor: extract standalone assertTelemetry helper --- integ-tests/add-remove-resources.test.ts | 12 +++--------- integ-tests/help.test.ts | 6 ++---- src/test-utils/audit.ts | 7 +++++++ src/test-utils/index.ts | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/integ-tests/add-remove-resources.test.ts b/integ-tests/add-remove-resources.test.ts index 844708741..7d340252e 100644 --- a/integ-tests/add-remove-resources.test.ts +++ b/integ-tests/add-remove-resources.test.ts @@ -1,4 +1,4 @@ -import { createAuditContext } from '../src/test-utils/audit.js'; +import { assertTelemetry, createAuditContext } from '../src/test-utils/audit.js'; import { createTestProject, readProjectConfig, runCLI } from '../src/test-utils/index.js'; import type { TestProject } from '../src/test-utils/index.js'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; @@ -42,10 +42,7 @@ describe('integration: add and remove resources', () => { expect(found, `Memory "${memoryName}" should be in config`).toBe(true); // Verify telemetry - const entries = audit.readEntries(); - const memEntry = entries.find(e => e.attrs.command === 'add.memory'); - expect(memEntry).toBeDefined(); - expect(memEntry!.attrs).toMatchObject({ command: 'add.memory', exit_reason: 'success' }); + assertTelemetry(audit.readEntries(), { command: 'add.memory', exit_reason: 'success' }); }); it('adds a memory with EPISODIC strategy and verifies reflectionNamespaces', async () => { @@ -114,10 +111,7 @@ describe('integration: add and remove resources', () => { expect(found, `Credential "${credentialName}" should be in config`).toBe(true); // Verify telemetry - const entries = audit.readEntries(); - const credEntry = entries.find(e => e.attrs.command === 'add.credential'); - expect(credEntry).toBeDefined(); - expect(credEntry!.attrs).toMatchObject({ + assertTelemetry(audit.readEntries(), { command: 'add.credential', exit_reason: 'success', credential_type: 'api-key', diff --git a/integ-tests/help.test.ts b/integ-tests/help.test.ts index 93aa447d3..4ed6706fd 100644 --- a/integ-tests/help.test.ts +++ b/integ-tests/help.test.ts @@ -1,4 +1,4 @@ -import { createAuditContext } from '../src/test-utils/audit.js'; +import { assertTelemetry, createAuditContext } from '../src/test-utils/audit.js'; import { spawnAndCollect } from '../src/test-utils/cli-runner.js'; import { runCLI } from '../src/test-utils/index.js'; import { readdirSync } from 'node:fs'; @@ -63,9 +63,7 @@ describe('help modes telemetry', () => { const entries = audit.readEntries(); expect(entries).toHaveLength(1); - expect(entries[0]!.attrs).toMatchObject({ - 'service.name': 'agentcore-cli', - 'agentcore-cli.mode': 'cli', + assertTelemetry(entries, { command_group: 'help', command: 'help.modes', exit_reason: 'success', diff --git a/src/test-utils/audit.ts b/src/test-utils/audit.ts index 857a00e05..79a6d1c27 100644 --- a/src/test-utils/audit.ts +++ b/src/test-utils/audit.ts @@ -2,6 +2,7 @@ import { globSync } from 'glob'; import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; +import { expect } from 'vitest'; export interface TelemetryEntry { value: number; @@ -42,3 +43,9 @@ export function createAuditContext(): AuditContext { }, }; } + +/** Assert that at least one telemetry entry was emitted matching the given attrs. */ +export function assertTelemetry(entries: TelemetryEntry[], expected: Record): void { + const match = entries.find(e => Object.entries(expected).every(([k, v]) => String(e.attrs[k]) === String(v))); + expect(match, `No telemetry entry matching ${JSON.stringify(expected)}`).toBeDefined(); +} diff --git a/src/test-utils/index.ts b/src/test-utils/index.ts index a9aad665d..a77a22d70 100644 --- a/src/test-utils/index.ts +++ b/src/test-utils/index.ts @@ -4,7 +4,7 @@ */ export { runCLI, spawnAndCollect, cleanSpawnEnv, type RunResult } from './cli-runner.js'; -export { createAuditContext, type AuditContext, type TelemetryEntry } from './audit.js'; +export { createAuditContext, assertTelemetry, type AuditContext, type TelemetryEntry } from './audit.js'; export { exists } from './fs-helpers.js'; export { hasCommand, hasAwsCredentials, prereqs } from './prereqs.js'; export { createTestProject, type TestProject, type CreateTestProjectOptions } from './project-factory.js'; From 8aecebfd103b79f5fc67000fd12276aff74dd359 Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Thu, 30 Apr 2026 00:30:17 +0000 Subject: [PATCH 36/45] refactor: rename audit.ts to telemetry-helper.ts, clarify method names --- integ-tests/add-remove-resources.test.ts | 14 +++++++------- integ-tests/help.test.ts | 16 ++++++++-------- src/test-utils/index.ts | 7 ++++++- .../{audit.ts => telemetry-helper.ts} | 18 +++++++++--------- 4 files changed, 30 insertions(+), 25 deletions(-) rename src/test-utils/{audit.ts => telemetry-helper.ts} (80%) diff --git a/integ-tests/add-remove-resources.test.ts b/integ-tests/add-remove-resources.test.ts index 7d340252e..7a43494ce 100644 --- a/integ-tests/add-remove-resources.test.ts +++ b/integ-tests/add-remove-resources.test.ts @@ -1,9 +1,9 @@ -import { assertTelemetry, createAuditContext } from '../src/test-utils/audit.js'; import { createTestProject, readProjectConfig, runCLI } from '../src/test-utils/index.js'; import type { TestProject } from '../src/test-utils/index.js'; +import { assertTelemetry, createTelemetryHelper } from '../src/test-utils/telemetry-helper.js'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; -const audit = createAuditContext(); +const telemetry = createTelemetryHelper(); describe('integration: add and remove resources', () => { let project: TestProject; @@ -19,7 +19,7 @@ describe('integration: add and remove resources', () => { afterAll(async () => { await project.cleanup(); - audit.cleanup(); + telemetry.destroy(); }); describe('memory lifecycle', () => { @@ -27,7 +27,7 @@ describe('integration: add and remove resources', () => { it('adds a memory resource', async () => { const result = await runCLI(['add', 'memory', '--name', memoryName, '--json'], project.projectPath, { - env: audit.env, + env: telemetry.env, }); expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); @@ -42,7 +42,7 @@ describe('integration: add and remove resources', () => { expect(found, `Memory "${memoryName}" should be in config`).toBe(true); // Verify telemetry - assertTelemetry(audit.readEntries(), { command: 'add.memory', exit_reason: 'success' }); + assertTelemetry(telemetry.readEntries(), { command: 'add.memory', exit_reason: 'success' }); }); it('adds a memory with EPISODIC strategy and verifies reflectionNamespaces', async () => { @@ -96,7 +96,7 @@ describe('integration: add and remove resources', () => { const result = await runCLI( ['add', 'credential', '--name', credentialName, '--api-key', 'test-key-integ-123', '--json'], project.projectPath, - { env: audit.env } + { env: telemetry.env } ); expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); @@ -111,7 +111,7 @@ describe('integration: add and remove resources', () => { expect(found, `Credential "${credentialName}" should be in config`).toBe(true); // Verify telemetry - assertTelemetry(audit.readEntries(), { + assertTelemetry(telemetry.readEntries(), { command: 'add.credential', exit_reason: 'success', credential_type: 'api-key', diff --git a/integ-tests/help.test.ts b/integ-tests/help.test.ts index 4ed6706fd..fe13fdf17 100644 --- a/integ-tests/help.test.ts +++ b/integ-tests/help.test.ts @@ -1,6 +1,6 @@ -import { assertTelemetry, createAuditContext } from '../src/test-utils/audit.js'; import { spawnAndCollect } from '../src/test-utils/cli-runner.js'; import { runCLI } from '../src/test-utils/index.js'; +import { assertTelemetry, createTelemetryHelper } from '../src/test-utils/telemetry-helper.js'; import { readdirSync } from 'node:fs'; import { join } from 'node:path'; import { afterAll, describe, expect, it } from 'vitest'; @@ -44,15 +44,15 @@ describe('CLI help', () => { }); describe('help modes telemetry', () => { - const audit = createAuditContext(); + const telemetry = createTelemetryHelper(); const cliPath = join(__dirname, '..', 'dist', 'cli', 'index.mjs'); - afterAll(() => audit.cleanup()); + afterAll(() => telemetry.destroy()); function run(args: string[], extraEnv: Record = {}) { return spawnAndCollect('node', [cliPath, ...args], process.cwd(), { AGENTCORE_SKIP_INSTALL: '1', - ...audit.env, + ...telemetry.env, ...extraEnv, }); } @@ -61,7 +61,7 @@ describe('help modes telemetry', () => { const result = await run(['help', 'modes']); expect(result.exitCode).toBe(0); - const entries = audit.readEntries(); + const entries = telemetry.readEntries(); expect(entries).toHaveLength(1); assertTelemetry(entries, { command_group: 'help', @@ -74,16 +74,16 @@ describe('help modes telemetry', () => { }); it('does not write audit file when audit is not enabled', async () => { - audit.clear(); + telemetry.clearEntries(); const noAuditCliPath = join(__dirname, '..', 'dist', 'cli', 'index.mjs'); const result = await spawnAndCollect('node', [noAuditCliPath, 'help', 'modes'], process.cwd(), { AGENTCORE_SKIP_INSTALL: '1', - AGENTCORE_CONFIG_DIR: audit.dir, + AGENTCORE_CONFIG_DIR: telemetry.dir, }); expect(result.exitCode).toBe(0); - const telemetryDir = join(audit.dir, 'telemetry'); + const telemetryDir = join(telemetry.dir, 'telemetry'); try { const files = readdirSync(telemetryDir); expect(files).toHaveLength(0); diff --git a/src/test-utils/index.ts b/src/test-utils/index.ts index a77a22d70..8802ea803 100644 --- a/src/test-utils/index.ts +++ b/src/test-utils/index.ts @@ -4,7 +4,12 @@ */ export { runCLI, spawnAndCollect, cleanSpawnEnv, type RunResult } from './cli-runner.js'; -export { createAuditContext, assertTelemetry, type AuditContext, type TelemetryEntry } from './audit.js'; +export { + createTelemetryHelper, + assertTelemetry, + type TelemetryHelper, + type TelemetryEntry, +} from './telemetry-helper.js'; export { exists } from './fs-helpers.js'; export { hasCommand, hasAwsCredentials, prereqs } from './prereqs.js'; export { createTestProject, type TestProject, type CreateTestProjectOptions } from './project-factory.js'; diff --git a/src/test-utils/audit.ts b/src/test-utils/telemetry-helper.ts similarity index 80% rename from src/test-utils/audit.ts rename to src/test-utils/telemetry-helper.ts index 79a6d1c27..e49c74d9c 100644 --- a/src/test-utils/audit.ts +++ b/src/test-utils/telemetry-helper.ts @@ -9,20 +9,20 @@ export interface TelemetryEntry { attrs: Record; } -export interface AuditContext { +export interface TelemetryHelper { /** Temp directory used as AGENTCORE_CONFIG_DIR */ dir: string; - /** Env vars to pass to runCLI */ + /** Env vars to pass to runCLI to enable audit mode */ env: { AGENTCORE_TELEMETRY_AUDIT: '1'; AGENTCORE_CONFIG_DIR: string }; /** Read all JSONL entries from the audit telemetry directory */ readEntries: () => TelemetryEntry[]; - /** Delete the telemetry subdirectory */ - clear: () => void; - /** Delete the entire config directory */ - cleanup: () => void; + /** Delete telemetry entries only (keeps the config dir) */ + clearEntries: () => void; + /** Delete the entire config directory — call in afterAll */ + destroy: () => void; } -export function createAuditContext(): AuditContext { +export function createTelemetryHelper(): TelemetryHelper { const dir = mkdtempSync(join(tmpdir(), 'agentcore-audit-')); return { dir, @@ -35,10 +35,10 @@ export function createAuditContext(): AuditContext { .map(line => JSON.parse(line) as TelemetryEntry) ); }, - clear() { + clearEntries() { rmSync(join(dir, 'telemetry'), { recursive: true, force: true }); }, - cleanup() { + destroy() { rmSync(dir, { recursive: true, force: true }); }, }; From 8e4a22942eda50d9d4ad8ba0166f2b4e31f73014 Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Thu, 30 Apr 2026 00:38:06 +0000 Subject: [PATCH 37/45] refactor: move assertTelemetry into TelemetryHelper as assertMetricEmitted --- integ-tests/add-remove-resources.test.ts | 6 +++--- integ-tests/help.test.ts | 4 ++-- src/test-utils/index.ts | 7 +------ src/test-utils/telemetry-helper.ts | 16 +++++++++------- 4 files changed, 15 insertions(+), 18 deletions(-) diff --git a/integ-tests/add-remove-resources.test.ts b/integ-tests/add-remove-resources.test.ts index 7a43494ce..67ff28637 100644 --- a/integ-tests/add-remove-resources.test.ts +++ b/integ-tests/add-remove-resources.test.ts @@ -1,6 +1,6 @@ import { createTestProject, readProjectConfig, runCLI } from '../src/test-utils/index.js'; import type { TestProject } from '../src/test-utils/index.js'; -import { assertTelemetry, createTelemetryHelper } from '../src/test-utils/telemetry-helper.js'; +import { createTelemetryHelper } from '../src/test-utils/telemetry-helper.js'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; const telemetry = createTelemetryHelper(); @@ -42,7 +42,7 @@ describe('integration: add and remove resources', () => { expect(found, `Memory "${memoryName}" should be in config`).toBe(true); // Verify telemetry - assertTelemetry(telemetry.readEntries(), { command: 'add.memory', exit_reason: 'success' }); + telemetry.assertMetricEmitted({ command: 'add.memory', exit_reason: 'success' }); }); it('adds a memory with EPISODIC strategy and verifies reflectionNamespaces', async () => { @@ -111,7 +111,7 @@ describe('integration: add and remove resources', () => { expect(found, `Credential "${credentialName}" should be in config`).toBe(true); // Verify telemetry - assertTelemetry(telemetry.readEntries(), { + telemetry.assertMetricEmitted({ command: 'add.credential', exit_reason: 'success', credential_type: 'api-key', diff --git a/integ-tests/help.test.ts b/integ-tests/help.test.ts index fe13fdf17..7e2176e2f 100644 --- a/integ-tests/help.test.ts +++ b/integ-tests/help.test.ts @@ -1,6 +1,6 @@ import { spawnAndCollect } from '../src/test-utils/cli-runner.js'; import { runCLI } from '../src/test-utils/index.js'; -import { assertTelemetry, createTelemetryHelper } from '../src/test-utils/telemetry-helper.js'; +import { createTelemetryHelper } from '../src/test-utils/telemetry-helper.js'; import { readdirSync } from 'node:fs'; import { join } from 'node:path'; import { afterAll, describe, expect, it } from 'vitest'; @@ -63,7 +63,7 @@ describe('help modes telemetry', () => { const entries = telemetry.readEntries(); expect(entries).toHaveLength(1); - assertTelemetry(entries, { + telemetry.assertMetricEmitted({ command_group: 'help', command: 'help.modes', exit_reason: 'success', diff --git a/src/test-utils/index.ts b/src/test-utils/index.ts index 8802ea803..1c032519c 100644 --- a/src/test-utils/index.ts +++ b/src/test-utils/index.ts @@ -4,12 +4,7 @@ */ export { runCLI, spawnAndCollect, cleanSpawnEnv, type RunResult } from './cli-runner.js'; -export { - createTelemetryHelper, - assertTelemetry, - type TelemetryHelper, - type TelemetryEntry, -} from './telemetry-helper.js'; +export { createTelemetryHelper, type TelemetryHelper, type TelemetryEntry } from './telemetry-helper.js'; export { exists } from './fs-helpers.js'; export { hasCommand, hasAwsCredentials, prereqs } from './prereqs.js'; export { createTestProject, type TestProject, type CreateTestProjectOptions } from './project-factory.js'; diff --git a/src/test-utils/telemetry-helper.ts b/src/test-utils/telemetry-helper.ts index e49c74d9c..9d7f70163 100644 --- a/src/test-utils/telemetry-helper.ts +++ b/src/test-utils/telemetry-helper.ts @@ -16,6 +16,8 @@ export interface TelemetryHelper { env: { AGENTCORE_TELEMETRY_AUDIT: '1'; AGENTCORE_CONFIG_DIR: string }; /** Read all JSONL entries from the audit telemetry directory */ readEntries: () => TelemetryEntry[]; + /** Assert a metric was emitted with attrs matching the given subset */ + assertMetricEmitted: (expected: Record) => void; /** Delete telemetry entries only (keeps the config dir) */ clearEntries: () => void; /** Delete the entire config directory — call in afterAll */ @@ -24,7 +26,7 @@ export interface TelemetryHelper { export function createTelemetryHelper(): TelemetryHelper { const dir = mkdtempSync(join(tmpdir(), 'agentcore-audit-')); - return { + const helper: TelemetryHelper = { dir, env: { AGENTCORE_TELEMETRY_AUDIT: '1', AGENTCORE_CONFIG_DIR: dir }, readEntries() { @@ -35,6 +37,11 @@ export function createTelemetryHelper(): TelemetryHelper { .map(line => JSON.parse(line) as TelemetryEntry) ); }, + assertMetricEmitted(expected) { + const entries = helper.readEntries(); + const match = entries.find(e => Object.entries(expected).every(([k, v]) => String(e.attrs[k]) === String(v))); + expect(match, `No telemetry entry matching ${JSON.stringify(expected)}`).toBeDefined(); + }, clearEntries() { rmSync(join(dir, 'telemetry'), { recursive: true, force: true }); }, @@ -42,10 +49,5 @@ export function createTelemetryHelper(): TelemetryHelper { rmSync(dir, { recursive: true, force: true }); }, }; -} - -/** Assert that at least one telemetry entry was emitted matching the given attrs. */ -export function assertTelemetry(entries: TelemetryEntry[], expected: Record): void { - const match = entries.find(e => Object.entries(expected).every(([k, v]) => String(e.attrs[k]) === String(v))); - expect(match, `No telemetry entry matching ${JSON.stringify(expected)}`).toBeDefined(); + return helper; } From 445a44832ab3c82d92d7c560c38d1e82963b934c Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Thu, 30 Apr 2026 13:04:11 +0000 Subject: [PATCH 38/45] feat: add telemetry to TUI add paths via withAddTelemetry --- src/cli/telemetry/cli-command-run.ts | 30 +++++++++++++ src/cli/tui/hooks/useCreateEvaluator.ts | 20 ++++++--- src/cli/tui/hooks/useCreateMcp.ts | 45 ++++++++++++------- src/cli/tui/hooks/useCreateMemory.ts | 27 ++++++++--- src/cli/tui/hooks/useCreateOnlineEval.ts | 23 +++++++--- .../tui/screens/identity/useCreateIdentity.ts | 9 +++- src/cli/tui/screens/policy/AddPolicyFlow.tsx | 33 ++++++++++---- .../AddRuntimeEndpointFlow.tsx | 27 +++++------ 8 files changed, 157 insertions(+), 57 deletions(-) diff --git a/src/cli/telemetry/cli-command-run.ts b/src/cli/telemetry/cli-command-run.ts index 4d3613e6e..02223b76e 100644 --- a/src/cli/telemetry/cli-command-run.ts +++ b/src/cli/telemetry/cli-command-run.ts @@ -1,4 +1,5 @@ import { getErrorMessage } from '../errors'; +import type { AddResult } from '../primitives/types.js'; import { TelemetryClientAccessor } from './client-accessor.js'; import type { Command, CommandAttrs } from './schemas/command-run.js'; @@ -35,3 +36,32 @@ export async function cliCommandRun( process.exit(1); } } + +/** + * Wrap a primitive .add() call with telemetry — used by TUI paths. + * CLI paths use {@link cliCommandRun} instead. + */ +export async function withAddTelemetry>( + command: C, + attrs: CommandAttrs, + fn: () => Promise> +): Promise> { + let client; + try { + client = await TelemetryClientAccessor.get(); + } catch { + return fn(); + } + + let result: AddResult | undefined; + try { + await client.withCommandRun(command, async () => { + result = await fn(); + if (!result.success) throw new Error(result.error); + return attrs; + }); + } catch { + if (!result) return fn(); + } + return result!; +} diff --git a/src/cli/tui/hooks/useCreateEvaluator.ts b/src/cli/tui/hooks/useCreateEvaluator.ts index 6e1d8f052..f1cad666f 100644 --- a/src/cli/tui/hooks/useCreateEvaluator.ts +++ b/src/cli/tui/hooks/useCreateEvaluator.ts @@ -1,5 +1,7 @@ import type { EvaluatorConfig } from '../../../schema'; import { evaluatorPrimitive } from '../../primitives/registry'; +import { withAddTelemetry } from '../../telemetry/cli-command-run.js'; +import { Level, standardize } from '../../telemetry/schemas/common-shapes.js'; import { useCallback, useEffect, useState } from 'react'; interface CreateEvaluatorConfig { @@ -16,11 +18,19 @@ export function useCreateEvaluator() { const create = useCallback(async (config: CreateEvaluatorConfig) => { setStatus({ state: 'loading' }); try { - const addResult = await evaluatorPrimitive.add({ - name: config.name, - level: config.level as 'SESSION' | 'TRACE' | 'TOOL_CALL', - config: config.config, - }); + const addResult = await withAddTelemetry( + 'add.evaluator', + { + evaluator_type: config.config.codeBased ? 'code-based' : 'llm-as-a-judge', + level: standardize(Level, config.level), + }, + () => + evaluatorPrimitive.add({ + name: config.name, + level: config.level as 'SESSION' | 'TRACE' | 'TOOL_CALL', + config: config.config, + }) + ); if (!addResult.success) { throw new Error(addResult.error ?? 'Failed to create evaluator'); } diff --git a/src/cli/tui/hooks/useCreateMcp.ts b/src/cli/tui/hooks/useCreateMcp.ts index 2b3b3b25a..ec91666d0 100644 --- a/src/cli/tui/hooks/useCreateMcp.ts +++ b/src/cli/tui/hooks/useCreateMcp.ts @@ -4,6 +4,8 @@ import { gatewayTargetPrimitive, policyEnginePrimitive, } from '../../primitives/registry'; +import { withAddTelemetry } from '../../telemetry/cli-command-run.js'; +import { AuthorizerType, PolicyEngineMode, standardize } from '../../telemetry/schemas/common-shapes.js'; import type { AddGatewayConfig } from '../screens/mcp/types'; import { useCallback, useEffect, useState } from 'react'; @@ -23,22 +25,33 @@ export function useCreateGateway() { const createGateway = useCallback(async (config: AddGatewayConfig) => { setStatus({ state: 'loading' }); try { - const addResult = await gatewayPrimitive.add({ - name: config.name, - description: config.description, - authorizerType: config.authorizerType, - discoveryUrl: config.jwtConfig?.discoveryUrl, - allowedAudience: config.jwtConfig?.allowedAudience?.join(','), - allowedClients: config.jwtConfig?.allowedClients?.join(','), - allowedScopes: config.jwtConfig?.allowedScopes?.join(','), - customClaims: config.jwtConfig?.customClaims, - clientId: config.jwtConfig?.clientId, - clientSecret: config.jwtConfig?.clientSecret, - enableSemanticSearch: config.enableSemanticSearch, - exceptionLevel: config.exceptionLevel, - policyEngine: config.policyEngineConfiguration?.policyEngineName, - policyEngineMode: config.policyEngineConfiguration?.mode, - }); + const addResult = await withAddTelemetry( + 'add.gateway', + { + authorizer_type: standardize(AuthorizerType, config.authorizerType ?? 'NONE'), + has_policy_engine: !!config.policyEngineConfiguration?.policyEngineName, + policy_engine_mode: standardize(PolicyEngineMode, config.policyEngineConfiguration?.mode ?? 'log_only'), + semantic_search: config.enableSemanticSearch !== false, + runtime_count: 0, + }, + () => + gatewayPrimitive.add({ + name: config.name, + description: config.description, + authorizerType: config.authorizerType, + discoveryUrl: config.jwtConfig?.discoveryUrl, + allowedAudience: config.jwtConfig?.allowedAudience?.join(','), + allowedClients: config.jwtConfig?.allowedClients?.join(','), + allowedScopes: config.jwtConfig?.allowedScopes?.join(','), + customClaims: config.jwtConfig?.customClaims, + clientId: config.jwtConfig?.clientId, + clientSecret: config.jwtConfig?.clientSecret, + enableSemanticSearch: config.enableSemanticSearch, + exceptionLevel: config.exceptionLevel, + policyEngine: config.policyEngineConfiguration?.policyEngineName, + policyEngineMode: config.policyEngineConfiguration?.mode, + }) + ); if (!addResult.success) { throw new Error(addResult.error ?? 'Failed to create gateway'); } diff --git a/src/cli/tui/hooks/useCreateMemory.ts b/src/cli/tui/hooks/useCreateMemory.ts index 4345b4ead..d4196582f 100644 --- a/src/cli/tui/hooks/useCreateMemory.ts +++ b/src/cli/tui/hooks/useCreateMemory.ts @@ -2,6 +2,7 @@ import { ConfigIO } from '../../../lib'; import type { Memory } from '../../../schema'; import { getAvailableAgents } from '../../operations/attach'; import { memoryPrimitive } from '../../primitives/registry'; +import { withAddTelemetry } from '../../telemetry/cli-command-run.js'; import { useCallback, useEffect, useState } from 'react'; interface CreateMemoryConfig { @@ -24,13 +25,25 @@ export function useCreateMemory() { setStatus({ state: 'loading' }); try { const strategiesStr = config.strategies.map(s => s.type).join(','); - const addResult = await memoryPrimitive.add({ - name: config.name, - expiry: config.eventExpiryDuration, - strategies: strategiesStr || undefined, - dataStreamArn: config.streaming?.dataStreamArn, - contentLevel: config.streaming?.contentLevel, - }); + const strategyList = strategiesStr ? strategiesStr.split(',').map(s => s.trim().toUpperCase()) : []; + const addResult = await withAddTelemetry( + 'add.memory', + { + strategy_count: strategyList.length, + strategy_semantic: strategyList.includes('SEMANTIC'), + strategy_summarization: strategyList.includes('SUMMARIZATION'), + strategy_user_preference: strategyList.includes('USER_PREFERENCE'), + strategy_episodic: strategyList.includes('EPISODIC'), + }, + () => + memoryPrimitive.add({ + name: config.name, + expiry: config.eventExpiryDuration, + strategies: strategiesStr || undefined, + dataStreamArn: config.streaming?.dataStreamArn, + contentLevel: config.streaming?.contentLevel, + }) + ); if (!addResult.success) { throw new Error(addResult.error ?? 'Failed to create memory'); } diff --git a/src/cli/tui/hooks/useCreateOnlineEval.ts b/src/cli/tui/hooks/useCreateOnlineEval.ts index 2d0190552..840b8ae5c 100644 --- a/src/cli/tui/hooks/useCreateOnlineEval.ts +++ b/src/cli/tui/hooks/useCreateOnlineEval.ts @@ -1,4 +1,5 @@ import { onlineEvalConfigPrimitive } from '../../primitives/registry'; +import { withAddTelemetry } from '../../telemetry/cli-command-run.js'; import { useCallback, useEffect, useState } from 'react'; interface CreateOnlineEvalConfig { @@ -17,13 +18,21 @@ export function useCreateOnlineEval() { const create = useCallback(async (config: CreateOnlineEvalConfig) => { setStatus({ state: 'loading' }); try { - const addResult = await onlineEvalConfigPrimitive.add({ - name: config.name, - agent: config.agent, - evaluators: config.evaluators, - samplingRate: config.samplingRate, - enableOnCreate: config.enableOnCreate, - }); + const addResult = await withAddTelemetry( + 'add.online-eval', + { + evaluator_count: config.evaluators.length, + enable_on_create: config.enableOnCreate ?? false, + }, + () => + onlineEvalConfigPrimitive.add({ + name: config.name, + agent: config.agent, + evaluators: config.evaluators, + samplingRate: config.samplingRate, + enableOnCreate: config.enableOnCreate, + }) + ); if (!addResult.success) { throw new Error(addResult.error ?? 'Failed to create online eval config'); } diff --git a/src/cli/tui/screens/identity/useCreateIdentity.ts b/src/cli/tui/screens/identity/useCreateIdentity.ts index 42aace21b..e214d1e67 100644 --- a/src/cli/tui/screens/identity/useCreateIdentity.ts +++ b/src/cli/tui/screens/identity/useCreateIdentity.ts @@ -2,6 +2,7 @@ import { ConfigIO } from '../../../../lib'; import type { Credential } from '../../../../schema'; import type { AddCredentialOptions } from '../../../primitives/CredentialPrimitive'; import { credentialPrimitive } from '../../../primitives/registry'; +import { withAddTelemetry } from '../../../telemetry/cli-command-run.js'; import { useCallback, useEffect, useState } from 'react'; interface CreateStatus { @@ -16,7 +17,13 @@ export function useCreateIdentity() { const create = useCallback(async (config: AddCredentialOptions) => { setStatus({ state: 'loading' }); try { - const result = await credentialPrimitive.add(config); + const result = await withAddTelemetry( + 'add.credential', + { + credential_type: config.authorizerType === 'OAuthCredentialProvider' ? 'oauth' : 'api-key', + }, + () => credentialPrimitive.add(config) + ); if (!result.success) { throw new Error(result.error ?? 'Failed to create credential'); } diff --git a/src/cli/tui/screens/policy/AddPolicyFlow.tsx b/src/cli/tui/screens/policy/AddPolicyFlow.tsx index 720984563..aa9110b08 100644 --- a/src/cli/tui/screens/policy/AddPolicyFlow.tsx +++ b/src/cli/tui/screens/policy/AddPolicyFlow.tsx @@ -1,4 +1,6 @@ import { policyEnginePrimitive, policyPrimitive } from '../../../primitives/registry'; +import { withAddTelemetry } from '../../../telemetry/cli-command-run.js'; +import { ValidationMode, standardize } from '../../../telemetry/schemas/common-shapes.js'; import { ErrorPrompt, Panel, @@ -128,7 +130,14 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD }, []); const commitEngine = useCallback(async (engineName: string, gateways?: string[], mode?: 'LOG_ONLY' | 'ENFORCE') => { - const result = await policyEnginePrimitive.add({ name: engineName }); + const result = await withAddTelemetry( + 'add.policy-engine', + { + attach_gateway_count: 0, + attach_mode: 'log_only', + }, + () => policyEnginePrimitive.add({ name: engineName }) + ); if (!result.success) { setFlow({ name: 'error', message: result.error }); return; @@ -155,13 +164,21 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD ); const handlePolicyComplete = useCallback(async (config: AddPolicyConfig) => { - const result = await policyPrimitive.add({ - name: config.name, - engine: config.engine, - statement: config.statement, - source: config.sourceFile || undefined, - validationMode: config.validationMode, - }); + const result = await withAddTelemetry( + 'add.policy', + { + source_type: config.sourceFile ? 'file' : 'statement', + validation_mode: standardize(ValidationMode, config.validationMode ?? 'FAIL_ON_ANY_FINDINGS'), + }, + () => + policyPrimitive.add({ + name: config.name, + engine: config.engine, + statement: config.statement, + source: config.sourceFile || undefined, + validationMode: config.validationMode, + }) + ); if (result.success) { setPolicyNames(prev => [...prev, config.name]); diff --git a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx index 2404109fc..83bda78e5 100644 --- a/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx +++ b/src/cli/tui/screens/runtime-endpoint/AddRuntimeEndpointFlow.tsx @@ -1,5 +1,6 @@ import { ConfigIO } from '../../../../lib'; import { runtimeEndpointPrimitive } from '../../../primitives/registry'; +import { withAddTelemetry } from '../../../telemetry/cli-command-run.js'; import { ErrorPrompt } from '../../components'; import { AddSuccessScreen } from '../add/AddSuccessScreen'; import { AddRuntimeEndpointScreen } from './AddRuntimeEndpointScreen'; @@ -78,24 +79,24 @@ export function AddRuntimeEndpointFlow({ }, [isInteractive, flow.name, onExit]); const handleCreateComplete = useCallback((config: RuntimeEndpointWizardConfig) => { - void runtimeEndpointPrimitive - .add({ + void withAddTelemetry('add.runtime-endpoint', {}, () => + runtimeEndpointPrimitive.add({ runtime: config.runtimeName, endpoint: config.endpointName, version: config.version, description: config.description, }) - .then(result => { - if (result.success) { - setFlow({ - name: 'create-success', - endpointName: config.endpointName, - runtimeName: config.runtimeName, - }); - return; - } - setFlow({ name: 'error', message: result.error ?? 'Unknown error' }); - }); + ).then(result => { + if (result.success) { + setFlow({ + name: 'create-success', + endpointName: config.endpointName, + runtimeName: config.runtimeName, + }); + return; + } + setFlow({ name: 'error', message: result.error ?? 'Unknown error' }); + }); }, []); if (flow.name === 'loading') { From c0af1ea5f08216897761daa446c9597ccad64e72 Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Thu, 30 Apr 2026 13:50:50 +0000 Subject: [PATCH 39/45] =?UTF-8?q?fix:=20review=20feedback=20=E2=80=94=20wi?= =?UTF-8?q?thAddTelemetry=20safety,=20standardize=20handles=20undefined,?= =?UTF-8?q?=20MCP=20agent=20attrs,=20policy=20TUI=20attrs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli/primitives/AgentPrimitive.tsx | 6 +++--- src/cli/telemetry/cli-command-run.ts | 5 +++-- src/cli/telemetry/schemas/common-shapes.ts | 4 ++-- src/cli/tui/screens/policy/AddPolicyFlow.tsx | 8 ++++---- src/test-utils/telemetry-helper.ts | 5 ++++- 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/cli/primitives/AgentPrimitive.tsx b/src/cli/primitives/AgentPrimitive.tsx index 3bd04d6ee..8f354a43c 100644 --- a/src/cli/primitives/AgentPrimitive.tsx +++ b/src/cli/primitives/AgentPrimitive.tsx @@ -342,9 +342,9 @@ export class AgentPrimitive extends BasePrimitive); } diff --git a/src/cli/telemetry/schemas/common-shapes.ts b/src/cli/telemetry/schemas/common-shapes.ts index 6ee451a10..f3dac5cfc 100644 --- a/src/cli/telemetry/schemas/common-shapes.ts +++ b/src/cli/telemetry/schemas/common-shapes.ts @@ -28,8 +28,8 @@ export function resilientParse( /** Lowercase a CLI value and parse it through a Zod enum, returning the narrowed type. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export function standardize>(schema: T, value: string): z.infer { - const lower = value.toLowerCase(); +export function standardize>(schema: T, value: string | undefined): z.infer { + const lower = (value ?? '').toLowerCase(); const result = schema.safeParse(lower); // If the value doesn't match the enum, return the lowercased value anyway — // recordCommandRun's try/catch will silently drop the invalid metric. diff --git a/src/cli/tui/screens/policy/AddPolicyFlow.tsx b/src/cli/tui/screens/policy/AddPolicyFlow.tsx index aa9110b08..9b3542cb8 100644 --- a/src/cli/tui/screens/policy/AddPolicyFlow.tsx +++ b/src/cli/tui/screens/policy/AddPolicyFlow.tsx @@ -1,6 +1,6 @@ import { policyEnginePrimitive, policyPrimitive } from '../../../primitives/registry'; import { withAddTelemetry } from '../../../telemetry/cli-command-run.js'; -import { ValidationMode, standardize } from '../../../telemetry/schemas/common-shapes.js'; +import { AttachMode, ValidationMode, standardize } from '../../../telemetry/schemas/common-shapes.js'; import { ErrorPrompt, Panel, @@ -133,8 +133,8 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD const result = await withAddTelemetry( 'add.policy-engine', { - attach_gateway_count: 0, - attach_mode: 'log_only', + attach_gateway_count: gateways?.length ?? 0, + attach_mode: standardize(AttachMode, mode ?? 'log_only'), }, () => policyEnginePrimitive.add({ name: engineName }) ); @@ -167,7 +167,7 @@ export function AddPolicyFlow({ isInteractive = true, onExit, onBack, onDev, onD const result = await withAddTelemetry( 'add.policy', { - source_type: config.sourceFile ? 'file' : 'statement', + source_type: config.sourceFile ? 'file' : config.sourceMethod === 'generate' ? 'generate' : 'statement', validation_mode: standardize(ValidationMode, config.validationMode ?? 'FAIL_ON_ANY_FINDINGS'), }, () => diff --git a/src/test-utils/telemetry-helper.ts b/src/test-utils/telemetry-helper.ts index 9d7f70163..e7fa58949 100644 --- a/src/test-utils/telemetry-helper.ts +++ b/src/test-utils/telemetry-helper.ts @@ -40,7 +40,10 @@ export function createTelemetryHelper(): TelemetryHelper { assertMetricEmitted(expected) { const entries = helper.readEntries(); const match = entries.find(e => Object.entries(expected).every(([k, v]) => String(e.attrs[k]) === String(v))); - expect(match, `No telemetry entry matching ${JSON.stringify(expected)}`).toBeDefined(); + expect( + match, + `No telemetry entry matching ${JSON.stringify(expected)}\nFound ${entries.length} entries:\n${entries.map(e => JSON.stringify(e.attrs)).join('\n')}` + ).toBeDefined(); }, clearEntries() { rmSync(join(dir, 'telemetry'), { recursive: true, force: true }); From db7300bfe12b4b0e47dcbd72663b07c81ed681f8 Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Thu, 30 Apr 2026 15:48:50 +0000 Subject: [PATCH 40/45] fix: remove unnecessary type assertion --- src/cli/primitives/EvaluatorPrimitive.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cli/primitives/EvaluatorPrimitive.ts b/src/cli/primitives/EvaluatorPrimitive.ts index 45459fec7..ec7372b08 100644 --- a/src/cli/primitives/EvaluatorPrimitive.ts +++ b/src/cli/primitives/EvaluatorPrimitive.ts @@ -316,7 +316,7 @@ export class EvaluatorPrimitive extends BasePrimitive Date: Thu, 30 Apr 2026 16:01:19 +0000 Subject: [PATCH 41/45] =?UTF-8?q?fix:=20address=20review=20=E2=80=94=20doc?= =?UTF-8?q?ument=20standardize=20cast,=20add=20policy-engine=20+=20episodi?= =?UTF-8?q?c=20telemetry=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- integ-tests/add-remove-resources.test.ts | 37 +++++++++++++++++++++- src/cli/telemetry/schemas/common-shapes.ts | 8 ++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/integ-tests/add-remove-resources.test.ts b/integ-tests/add-remove-resources.test.ts index 67ff28637..a89c761dd 100644 --- a/integ-tests/add-remove-resources.test.ts +++ b/integ-tests/add-remove-resources.test.ts @@ -49,7 +49,8 @@ describe('integration: add and remove resources', () => { const episodicMemName = `EpiMem${Date.now().toString().slice(-6)}`; const result = await runCLI( ['add', 'memory', '--name', episodicMemName, '--strategies', 'EPISODIC', '--json'], - project.projectPath + project.projectPath, + { env: telemetry.env } ); expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); @@ -70,6 +71,14 @@ describe('integration: add and remove resources', () => { expect(episodic!.reflectionNamespaces, 'Should have reflectionNamespaces').toBeDefined(); expect(episodic!.reflectionNamespaces!.length).toBeGreaterThan(0); + // Verify telemetry + telemetry.assertMetricEmitted({ + command: 'add.memory', + exit_reason: 'success', + strategy_count: '1', + strategy_episodic: 'true', + }); + // Clean up await runCLI(['remove', 'memory', '--name', episodicMemName, '--json'], project.projectPath); }); @@ -132,4 +141,30 @@ describe('integration: add and remove resources', () => { expect(found, `Credential "${credentialName}" should be removed from config`).toBe(false); }); }); + + describe('policy-engine', () => { + const engineName = `TestEngine${Date.now().toString().slice(-6)}`; + + it('adds a policy engine resource', async () => { + const result = await runCLI(['add', 'policy-engine', '--name', engineName, '--json'], project.projectPath, { + env: telemetry.env, + }); + + expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); + const json = JSON.parse(result.stdout); + expect(json.success).toBe(true); + + telemetry.assertMetricEmitted({ + command: 'add.policy-engine', + exit_reason: 'success', + attach_gateway_count: '0', + }); + }); + + it('removes the policy engine resource', async () => { + const result = await runCLI(['remove', 'policy-engine', '--name', engineName, '--json'], project.projectPath); + + expect(result.exitCode, `stdout: ${result.stdout}, stderr: ${result.stderr}`).toBe(0); + }); + }); }); diff --git a/src/cli/telemetry/schemas/common-shapes.ts b/src/cli/telemetry/schemas/common-shapes.ts index f3dac5cfc..73568f907 100644 --- a/src/cli/telemetry/schemas/common-shapes.ts +++ b/src/cli/telemetry/schemas/common-shapes.ts @@ -26,7 +26,13 @@ export function resilientParse( return result; } -/** Lowercase a CLI value and parse it through a Zod enum, returning the narrowed type. */ +/** + * Lowercase a CLI value and parse it through a Zod enum, returning the narrowed type. + * The `as` cast on the failure branch is intentional: invalid values pass through to + * recordCommandRun, where COMMAND_SCHEMAS[command].parse(attrs) validates the full + * attr object in a try/catch — silently dropping the metric if any field is invalid. + * This ensures telemetry never crashes the CLI while keeping the happy-path type-safe. + */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export function standardize>(schema: T, value: string | undefined): z.infer { const lower = (value ?? '').toLowerCase(); From 9087af05824c4f5e34390a02c5fcd8b2574f52a1 Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Thu, 30 Apr 2026 16:10:12 +0000 Subject: [PATCH 42/45] refactor: centralize gateway target type mapping in common-shapes --- src/cli/primitives/GatewayTargetPrimitive.ts | 20 +++++++------------- src/cli/telemetry/schemas/common-shapes.ts | 9 +++++++++ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/cli/primitives/GatewayTargetPrimitive.ts b/src/cli/primitives/GatewayTargetPrimitive.ts index 95d237e65..56812f6e8 100644 --- a/src/cli/primitives/GatewayTargetPrimitive.ts +++ b/src/cli/primitives/GatewayTargetPrimitive.ts @@ -15,7 +15,12 @@ import { getErrorMessage } from '../errors'; import type { RemovableGatewayTarget } from '../operations/remove/remove-gateway-target'; import type { RemovalPreview, RemovalResult, SchemaChange } from '../operations/remove/types'; import { cliCommandRun } from '../telemetry/cli-command-run.js'; -import { GatewayTargetHost, OutboundAuth, standardize } from '../telemetry/schemas/common-shapes.js'; +import { + GATEWAY_TARGET_TYPE_MAP, + GatewayTargetHost, + OutboundAuth, + standardize, +} from '../telemetry/schemas/common-shapes.js'; import { getTemplateToolDefinitions, renderGatewayTargetTemplate } from '../templates/GatewayTargetRenderer'; import { requireTTY } from '../tui/guards/tty'; import type { @@ -318,19 +323,8 @@ export class GatewayTargetPrimitive extends BasePrimitive> = { + apiGateway: 'api-gateway', + openApiSchema: 'open-api-schema', + smithyModel: 'smithy-model', + lambdaFunctionArn: 'lambda-function-arn', + mcpServer: 'mcp-server', +}; export const Language = z.enum(['python', 'typescript', 'other']); export const Level = z.enum(['session', 'trace', 'tool_call']); export const Memory = z.enum(['none', 'shortterm', 'longandshortterm']); From 35a0531c38cad79e8f80c8767cebe15d63ad03db Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Thu, 30 Apr 2026 17:05:07 +0000 Subject: [PATCH 43/45] fix: preserve original function error with telemetry wrapper --- src/cli/telemetry/cli-command-run.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/cli/telemetry/cli-command-run.ts b/src/cli/telemetry/cli-command-run.ts index e5fe0a84c..987f05730 100644 --- a/src/cli/telemetry/cli-command-run.ts +++ b/src/cli/telemetry/cli-command-run.ts @@ -60,9 +60,12 @@ export async function withAddTelemetry); + return result!; } From 0d09e63c4b7e0ee66b9e12a82c3238c96c03ea50 Mon Sep 17 00:00:00 2001 From: Harrison Weinstock Date: Thu, 30 Apr 2026 17:12:00 +0000 Subject: [PATCH 44/45] refactor: extract telemetryAttrs into a single line --- src/cli/primitives/GatewayTargetPrimitive.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/cli/primitives/GatewayTargetPrimitive.ts b/src/cli/primitives/GatewayTargetPrimitive.ts index 56812f6e8..418455b33 100644 --- a/src/cli/primitives/GatewayTargetPrimitive.ts +++ b/src/cli/primitives/GatewayTargetPrimitive.ts @@ -330,6 +330,11 @@ export class GatewayTargetPrimitive extends BasePrimitive Date: Thu, 30 Apr 2026 17:23:26 +0000 Subject: [PATCH 45/45] feat: wire up telemetry for addAgent --- src/cli/primitives/GatewayTargetPrimitive.ts | 2 +- src/cli/tui/screens/agent/useAddAgent.ts | 96 ++++++++++++++------ 2 files changed, 69 insertions(+), 29 deletions(-) diff --git a/src/cli/primitives/GatewayTargetPrimitive.ts b/src/cli/primitives/GatewayTargetPrimitive.ts index 418455b33..e8a1da996 100644 --- a/src/cli/primitives/GatewayTargetPrimitive.ts +++ b/src/cli/primitives/GatewayTargetPrimitive.ts @@ -427,7 +427,7 @@ export class GatewayTargetPrimitive extends BasePrimitive => { setIsLoading(true); try { - const configBaseDir = findConfigRoot(); - if (!configBaseDir) { - return { ok: false, error: new NoProjectError().message }; - } - - const configIO = new ConfigIO({ baseDir: configBaseDir }); - - if (!configIO.configExists('project')) { - return { ok: false, error: new NoProjectError().message }; - } - - // Check for duplicate agent name - const project = await configIO.readProjectSpec(); - const existingAgent = project.runtimes.find(agent => agent.name === config.name); - if (existingAgent) { - return { ok: false, error: `Agent "${config.name}" already exists in this project.` }; - } - - // Branch based on agent type - if (config.agentType === 'import') { - return await handleImportPath(config, configBaseDir); - } else if (config.agentType === 'create') { - return await handleCreatePath(config, configBaseDir); - } else { - return await handleByoPath(config, configIO, configBaseDir); + const result = await withAddTelemetry( + 'add.agent', + { + language: standardize(Language, config.language), + framework: standardize(Framework, config.framework), + model_provider: standardize(ModelProvider, config.modelProvider), + agent_type: standardize(AgentTypeEnum, config.agentType), + build: standardize(Build, config.buildType), + protocol: standardize(Protocol, config.protocol ?? 'HTTP'), + network_mode: standardize(NetworkMode, config.networkMode ?? 'PUBLIC'), + authorizer_type: standardize(AuthorizerTypeEnum, config.authorizerType ?? 'NONE'), + memory: standardize(MemoryEnum, config.memory ?? 'none'), + }, + () => addAgentInner(config) + ); + if (!result.success) { + return { ok: false, error: result.error }; } - } catch (err) { - return { ok: false, error: getErrorMessage(err) }; + return result.outcome; } finally { setIsLoading(false); } @@ -175,6 +178,43 @@ export function useAddAgent() { return { addAgent, isLoading, reset }; } +type AddAgentInnerResult = + | { success: true; outcome: AddAgentCreateResult | AddAgentByoResult } + | { success: false; error: string }; + +async function addAgentInner(config: AddAgentConfig): Promise { + const configBaseDir = findConfigRoot(); + if (!configBaseDir) { + return { success: false, error: new NoProjectError().message }; + } + + const configIO = new ConfigIO({ baseDir: configBaseDir }); + + if (!configIO.configExists('project')) { + return { success: false, error: new NoProjectError().message }; + } + + const project = await configIO.readProjectSpec(); + const existingAgent = project.runtimes.find(agent => agent.name === config.name); + if (existingAgent) { + return { success: false, error: `Agent "${config.name}" already exists in this project.` }; + } + + let outcome: AddAgentCreateResult | AddAgentByoResult | AddAgentError; + if (config.agentType === 'import') { + outcome = await handleImportPath(config, configBaseDir); + } else if (config.agentType === 'create') { + outcome = await handleCreatePath(config, configBaseDir); + } else { + outcome = await handleByoPath(config, configIO, configBaseDir); + } + + if (!outcome.ok) { + return { success: false, error: outcome.error }; + } + return { success: true, outcome }; +} + /** * Handle the "create" path: generate agent from template and write to project. */