diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 913979af01..44e00af382 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1297,12 +1297,12 @@ importers: services/apps/packages_worker: dependencies: - '@anthropic-ai/claude-agent-sdk': - specifier: ^0.3.216 - version: 0.3.217(@anthropic-ai/sdk@0.112.5(zod@4.3.6))(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(zod@4.3.6) '@aws-sdk/client-bedrock-runtime': specifier: ^3.572.0 version: 3.572.0 + '@crowd/anthropic-aws': + specifier: workspace:* + version: link:../../libs/anthropic-aws '@crowd/archetype-standard': specifier: workspace:* version: link:../../archetypes/standard @@ -2061,6 +2061,22 @@ importers: specifier: ^5.6.3 version: 5.6.3 + services/libs/anthropic-aws: + dependencies: + '@anthropic-ai/claude-agent-sdk': + specifier: ^0.3.216 + version: 0.3.217(@anthropic-ai/sdk@0.112.5(zod@4.3.6))(@modelcontextprotocol/sdk@1.29.0(zod@4.3.6))(zod@4.3.6) + '@crowd/logging': + specifier: workspace:* + version: link:../logging + devDependencies: + '@types/node': + specifier: ^20.8.2 + version: 20.12.7 + typescript: + specifier: ^5.6.3 + version: 5.6.3 + services/libs/audit-logs: dependencies: '@crowd/data-access-layer': @@ -10953,8 +10969,8 @@ snapshots: dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.572.0 - '@aws-sdk/client-sts': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) + '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0) + '@aws-sdk/client-sts': 3.572.0 '@aws-sdk/core': 3.572.0 '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0) '@aws-sdk/middleware-host-header': 3.567.0 @@ -11148,11 +11164,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sso-oidc@3.572.0': + '@aws-sdk/client-sso-oidc@3.572.0(@aws-sdk/client-sts@3.572.0)': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sts': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) + '@aws-sdk/client-sts': 3.572.0 '@aws-sdk/core': 3.572.0 '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0) '@aws-sdk/middleware-host-header': 3.567.0 @@ -11191,6 +11207,7 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.6.2 transitivePeerDependencies: + - '@aws-sdk/client-sts' - aws-crt '@aws-sdk/client-sso@3.556.0': @@ -11366,11 +11383,11 @@ snapshots: transitivePeerDependencies: - aws-crt - '@aws-sdk/client-sts@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)': + '@aws-sdk/client-sts@3.572.0': dependencies: '@aws-crypto/sha256-browser': 3.0.0 '@aws-crypto/sha256-js': 3.0.0 - '@aws-sdk/client-sso-oidc': 3.572.0 + '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0) '@aws-sdk/core': 3.572.0 '@aws-sdk/credential-provider-node': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0) '@aws-sdk/middleware-host-header': 3.567.0 @@ -11409,7 +11426,6 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.6.2 transitivePeerDependencies: - - '@aws-sdk/client-sso-oidc' - aws-crt '@aws-sdk/client-sts@3.985.0': @@ -11575,7 +11591,7 @@ snapshots: '@aws-sdk/credential-provider-ini@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)(@aws-sdk/client-sts@3.572.0)': dependencies: - '@aws-sdk/client-sts': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) + '@aws-sdk/client-sts': 3.572.0 '@aws-sdk/credential-provider-env': 3.568.0 '@aws-sdk/credential-provider-process': 3.572.0 '@aws-sdk/credential-provider-sso': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) @@ -11752,7 +11768,7 @@ snapshots: '@aws-sdk/credential-provider-web-identity@3.568.0(@aws-sdk/client-sts@3.572.0)': dependencies: - '@aws-sdk/client-sts': 3.572.0(@aws-sdk/client-sso-oidc@3.572.0) + '@aws-sdk/client-sts': 3.572.0 '@aws-sdk/types': 3.567.0 '@smithy/property-provider': 2.2.0 '@smithy/types': 2.12.0 @@ -12064,7 +12080,7 @@ snapshots: '@aws-sdk/token-providers@3.572.0(@aws-sdk/client-sso-oidc@3.572.0)': dependencies: - '@aws-sdk/client-sso-oidc': 3.572.0 + '@aws-sdk/client-sso-oidc': 3.572.0(@aws-sdk/client-sts@3.572.0) '@aws-sdk/types': 3.567.0 '@smithy/property-provider': 2.2.0 '@smithy/shared-ini-file-loader': 2.4.0 diff --git a/services/apps/packages_worker/package.json b/services/apps/packages_worker/package.json index abad411c0a..73fa9912f0 100644 --- a/services/apps/packages_worker/package.json +++ b/services/apps/packages_worker/package.json @@ -84,13 +84,13 @@ "@crowd/archetype-standard": "workspace:*", "@crowd/archetype-worker": "workspace:*", "@crowd/temporal": "workspace:*", + "@crowd/anthropic-aws": "workspace:*", "@crowd/common": "workspace:*", "@crowd/data-access-layer": "workspace:*", "@crowd/database": "workspace:*", "@crowd/logging": "workspace:*", "@crowd/slack": "workspace:*", "@crowd/types": "workspace:*", - "@anthropic-ai/claude-agent-sdk": "^0.3.216", "@aws-sdk/client-bedrock-runtime": "^3.572.0", "@dsnp/parquetjs": "^1.7.0", "@google-cloud/bigquery": "^8.3.1", diff --git a/services/apps/packages_worker/src/blast-radius/agent/__tests__/agentAuth.test.ts b/services/apps/packages_worker/src/blast-radius/agent/__tests__/agentAuth.test.ts deleted file mode 100644 index 94dca4b004..0000000000 --- a/services/apps/packages_worker/src/blast-radius/agent/__tests__/agentAuth.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { afterEach, describe, expect, it } from 'vitest' - -import { resolveAgentAuth } from '@crowd/common' - -const AKRITES_BEDROCK_ENV_VAR_NAMES = { - accessKeyId: 'AKRITES_AWS_BEDROCK_ACCESS_KEY_ID', - secretAccessKey: 'AKRITES_AWS_BEDROCK_SECRET_ACCESS_KEY', - region: 'AKRITES_AWS_BEDROCK_REGION', -} - -const ALL_ENV_VARS = [ - 'CROWD_AWS_BEDROCK_ACCESS_KEY_ID', - 'CROWD_AWS_BEDROCK_SECRET_ACCESS_KEY', - 'CROWD_AWS_BEDROCK_REGION', - 'AKRITES_AWS_BEDROCK_ACCESS_KEY_ID', - 'AKRITES_AWS_BEDROCK_SECRET_ACCESS_KEY', - 'AKRITES_AWS_BEDROCK_REGION', - 'BLAST_RADIUS_ANTHROPIC_API_KEY', - 'BLAST_RADIUS_ANTHROPIC_BASE_URL', - 'ANTHROPIC_API_KEY', -] - -function clearAuthEnv() { - for (const key of ALL_ENV_VARS) { - delete process.env[key] - } -} - -describe('resolveAgentAuth', () => { - afterEach(() => { - clearAuthEnv() - }) - - it('resolves bedrock mode using the default (CROWD_AWS_BEDROCK_*) env vars', () => { - clearAuthEnv() - process.env.CROWD_AWS_BEDROCK_ACCESS_KEY_ID = 'AKIA_TEST' - process.env.CROWD_AWS_BEDROCK_SECRET_ACCESS_KEY = 'secret' - process.env.ANTHROPIC_API_KEY = 'sk-ant-should-not-survive' - - const auth = resolveAgentAuth() - - expect(auth.mode).toBe('bedrock') - expect(auth.env.CLAUDE_CODE_USE_BEDROCK).toBe('1') - expect(auth.env.AWS_ACCESS_KEY_ID).toBe('AKIA_TEST') - expect(auth.env.AWS_SECRET_ACCESS_KEY).toBe('secret') - expect(auth.env.AWS_REGION).toBe('us-east-1') - expect(auth.env.ANTHROPIC_API_KEY).toBeUndefined() - }) - - it('resolves bedrock mode using a caller-supplied bedrockEnvVarNames (e.g. Akrites)', () => { - clearAuthEnv() - process.env.AKRITES_AWS_BEDROCK_ACCESS_KEY_ID = 'AKIA_AKRITES' - process.env.AKRITES_AWS_BEDROCK_SECRET_ACCESS_KEY = 'akrites-secret' - // A CROWD_AWS_BEDROCK_* credential being set for an unrelated consumer must not - // leak into an Akrites-scoped caller. - process.env.CROWD_AWS_BEDROCK_ACCESS_KEY_ID = 'AKIA_UNRELATED' - process.env.CROWD_AWS_BEDROCK_SECRET_ACCESS_KEY = 'unrelated-secret' - - const auth = resolveAgentAuth({ bedrockEnvVarNames: AKRITES_BEDROCK_ENV_VAR_NAMES }) - - expect(auth.mode).toBe('bedrock') - expect(auth.env.AWS_ACCESS_KEY_ID).toBe('AKIA_AKRITES') - expect(auth.env.AWS_SECRET_ACCESS_KEY).toBe('akrites-secret') - }) - - it('uses the region env var named by bedrockEnvVarNames when set', () => { - clearAuthEnv() - process.env.AKRITES_AWS_BEDROCK_ACCESS_KEY_ID = 'AKIA_AKRITES' - process.env.AKRITES_AWS_BEDROCK_SECRET_ACCESS_KEY = 'akrites-secret' - process.env.AKRITES_AWS_BEDROCK_REGION = 'us-west-2' - - const auth = resolveAgentAuth({ bedrockEnvVarNames: AKRITES_BEDROCK_ENV_VAR_NAMES }) - - expect(auth.env.AWS_REGION).toBe('us-west-2') - }) - - it('does not fall into bedrock mode with only one of the two credentials', () => { - clearAuthEnv() - process.env.AKRITES_AWS_BEDROCK_ACCESS_KEY_ID = 'AKIA_AKRITES' - process.env.BLAST_RADIUS_ANTHROPIC_API_KEY = 'sk-ant-fallback' - - const auth = resolveAgentAuth({ bedrockEnvVarNames: AKRITES_BEDROCK_ENV_VAR_NAMES }) - - expect(auth.mode).toBe('anthropic-api-key') - }) - - it('resolves anthropic-api-key mode when no bedrock credentials are set', () => { - clearAuthEnv() - process.env.BLAST_RADIUS_ANTHROPIC_API_KEY = 'sk-ant-test' - process.env.BLAST_RADIUS_ANTHROPIC_BASE_URL = 'https://litellm.internal' - - const auth = resolveAgentAuth({ bedrockEnvVarNames: AKRITES_BEDROCK_ENV_VAR_NAMES }) - - expect(auth.mode).toBe('anthropic-api-key') - expect(auth.env.ANTHROPIC_API_KEY).toBe('sk-ant-test') - expect(auth.env.ANTHROPIC_BASE_URL).toBe('https://litellm.internal') - expect(auth.resolveModel('claude-opus-4-8')).toBe('claude-opus-4-8') - }) - - it('supports a custom api key env var name', () => { - clearAuthEnv() - process.env.CUSTOM_ANTHROPIC_API_KEY = 'sk-ant-custom' - - const auth = resolveAgentAuth({ apiKeyEnvVar: 'CUSTOM_ANTHROPIC_API_KEY' }) - - expect(auth.mode).toBe('anthropic-api-key') - expect(auth.env.ANTHROPIC_API_KEY).toBe('sk-ant-custom') - - delete process.env.CUSTOM_ANTHROPIC_API_KEY - }) - - it('falls back to cli auth when nothing is configured', () => { - clearAuthEnv() - - const auth = resolveAgentAuth({ bedrockEnvVarNames: AKRITES_BEDROCK_ENV_VAR_NAMES }) - - expect(auth.mode).toBe('cli-fallback') - expect(auth.env).toBeUndefined() - expect(auth.resolveModel('claude-sonnet-5')).toBe('claude-sonnet-5') - }) - - it('translates model IDs via modelBedrockMap in bedrock mode', () => { - clearAuthEnv() - process.env.AKRITES_AWS_BEDROCK_ACCESS_KEY_ID = 'AKIA_AKRITES' - process.env.AKRITES_AWS_BEDROCK_SECRET_ACCESS_KEY = 'akrites-secret' - - const auth = resolveAgentAuth({ - bedrockEnvVarNames: AKRITES_BEDROCK_ENV_VAR_NAMES, - modelBedrockMap: { 'claude-opus-4-8': 'us.anthropic.claude-opus-4-8-v1:0' }, - }) - - expect(auth.resolveModel('claude-opus-4-8')).toBe('us.anthropic.claude-opus-4-8-v1:0') - }) - - it('throws on an unmapped model when a modelBedrockMap is provided', () => { - clearAuthEnv() - process.env.AKRITES_AWS_BEDROCK_ACCESS_KEY_ID = 'AKIA_AKRITES' - process.env.AKRITES_AWS_BEDROCK_SECRET_ACCESS_KEY = 'akrites-secret' - - const auth = resolveAgentAuth({ - bedrockEnvVarNames: AKRITES_BEDROCK_ENV_VAR_NAMES, - modelBedrockMap: { 'claude-opus-4-8': 'us.anthropic.claude-opus-4-8-v1:0' }, - }) - - expect(() => auth.resolveModel('claude-haiku-9000')).toThrow() - }) -}) diff --git a/services/apps/packages_worker/src/blast-radius/stages/__tests__/reachabilityStage.test.ts b/services/apps/packages_worker/src/blast-radius/stages/__tests__/reachabilityStage.test.ts index ea92507c30..63900eee3a 100644 --- a/services/apps/packages_worker/src/blast-radius/stages/__tests__/reachabilityStage.test.ts +++ b/services/apps/packages_worker/src/blast-radius/stages/__tests__/reachabilityStage.test.ts @@ -1,8 +1,8 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' +import { runClaudeAgentQuery } from '@crowd/anthropic-aws' import * as blastRadiusDal from '@crowd/data-access-layer/src/packages/blastRadius' -import { runAnalysisAgent } from '../../agent/runner' import { ReachabilitySourceConfig, runReachabilityStage } from '../reachabilityStage' vi.mock('@crowd/data-access-layer/src/packages/blastRadius', () => ({ @@ -15,7 +15,7 @@ vi.mock('@crowd/data-access-layer/src/packages/blastRadius', () => ({ completeStageRun: vi.fn(), failStageRun: vi.fn(), })) -vi.mock('../../agent/runner', () => ({ runAnalysisAgent: vi.fn() })) +vi.mock('@crowd/anthropic-aws', () => ({ runClaudeAgentQuery: vi.fn() })) const qx = {} as never const SYMBOL_SPEC_ROW = { @@ -71,7 +71,7 @@ describe('runReachabilityStage', () => { qx, expect.objectContaining({ dependentId: 'dep-1', reasoning: 'no source', model: null }), ) - expect(runAnalysisAgent).not.toHaveBeenCalled() + expect(runClaudeAgentQuery).not.toHaveBeenCalled() }) it('persists an error verdict with downloadErrorPrefix when download throws', async () => { @@ -89,11 +89,11 @@ describe('runReachabilityStage', () => { reasoning: 'download failed: boom', }), ) - expect(runAnalysisAgent).not.toHaveBeenCalled() + expect(runClaudeAgentQuery).not.toHaveBeenCalled() }) it('persists the mapped verdict on a successful agent run', async () => { - vi.mocked(runAnalysisAgent).mockResolvedValue({ + vi.mocked(runClaudeAgentQuery).mockResolvedValue({ structuredOutput: { uses_package: true, imports_vulnerable_symbol: true, @@ -127,7 +127,7 @@ describe('runReachabilityStage', () => { }) it('retries on agent error and succeeds on a later attempt', async () => { - vi.mocked(runAnalysisAgent) + vi.mocked(runClaudeAgentQuery) .mockResolvedValueOnce({ structuredOutput: null, isError: true, @@ -153,7 +153,7 @@ describe('runReachabilityStage', () => { await runReachabilityStage(qx, 'analysis-1', makeConfig()) - expect(runAnalysisAgent).toHaveBeenCalledTimes(2) + expect(runClaudeAgentQuery).toHaveBeenCalledTimes(2) expect(blastRadiusDal.upsertVerdict).toHaveBeenCalledWith( qx, expect.objectContaining({ dependentId: 'dep-1', reachableVerdict: 'not_affected' }), diff --git a/services/apps/packages_worker/src/blast-radius/stages/cargo/intelCargo.ts b/services/apps/packages_worker/src/blast-radius/stages/cargo/intelCargo.ts index d4b338589f..acbb8325af 100644 --- a/services/apps/packages_worker/src/blast-radius/stages/cargo/intelCargo.ts +++ b/services/apps/packages_worker/src/blast-radius/stages/cargo/intelCargo.ts @@ -2,6 +2,7 @@ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' +import { runClaudeAgentQuery } from '@crowd/anthropic-aws' import * as blastRadiusDal from '@crowd/data-access-layer/src/packages/blastRadius' import { getVersionNumbers } from '@crowd/data-access-layer/src/packages/blastRadiusDependents' import { findPackageId } from '@crowd/data-access-layer/src/packages/osv' @@ -12,7 +13,6 @@ import { CARGO_INTEL_SYSTEM_PROMPT, buildCargoIntelPrompt, } from '../../agent/cargoPrompts' -import { runAnalysisAgent } from '../../agent/runner' import { fetchPatch } from '../../clients/githubPatch' import { downloadAndExtractTarball } from '../../clients/npmTarball' import { @@ -135,7 +135,7 @@ export async function runIntelStageCargo( patches, ) - const agentResult = await runAnalysisAgent({ + const agentResult = await runClaudeAgentQuery({ prompt: agentPrompt, systemPrompt: CARGO_INTEL_SYSTEM_PROMPT, cwd: pkgsrcDir, diff --git a/services/apps/packages_worker/src/blast-radius/stages/go/intelGo.ts b/services/apps/packages_worker/src/blast-radius/stages/go/intelGo.ts index c5de0c9e5e..d89b6ae017 100644 --- a/services/apps/packages_worker/src/blast-radius/stages/go/intelGo.ts +++ b/services/apps/packages_worker/src/blast-radius/stages/go/intelGo.ts @@ -2,6 +2,7 @@ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' +import { runClaudeAgentQuery } from '@crowd/anthropic-aws' import * as blastRadiusDal from '@crowd/data-access-layer/src/packages/blastRadius' import { getVersionNumbers } from '@crowd/data-access-layer/src/packages/blastRadiusDependents' import { findPackageId } from '@crowd/data-access-layer/src/packages/osv' @@ -9,7 +10,6 @@ import { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor' import { fetchVersionList } from '../../../go/proxyClient' import { GO_INTEL_SCHEMA, GO_INTEL_SYSTEM_PROMPT, buildGoIntelPrompt } from '../../agent/goPrompts' -import { runAnalysisAgent } from '../../agent/runner' import { fetchPatch } from '../../clients/githubPatch' import { downloadAndExtractGoModule } from '../../clients/goModuleZip' import { @@ -125,7 +125,7 @@ export async function runIntelStageGo( patches, ) - const agentResult = await runAnalysisAgent({ + const agentResult = await runClaudeAgentQuery({ prompt: agentPrompt, systemPrompt: GO_INTEL_SYSTEM_PROMPT, cwd: pkgsrcDir, diff --git a/services/apps/packages_worker/src/blast-radius/stages/maven/intelMaven.ts b/services/apps/packages_worker/src/blast-radius/stages/maven/intelMaven.ts index 60a3450324..2a8fe1456c 100644 --- a/services/apps/packages_worker/src/blast-radius/stages/maven/intelMaven.ts +++ b/services/apps/packages_worker/src/blast-radius/stages/maven/intelMaven.ts @@ -2,6 +2,7 @@ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' +import { runClaudeAgentQuery } from '@crowd/anthropic-aws' import * as blastRadiusDal from '@crowd/data-access-layer/src/packages/blastRadius' import { getVersionNumbers } from '@crowd/data-access-layer/src/packages/blastRadiusDependents' import { findPackageId } from '@crowd/data-access-layer/src/packages/osv' @@ -14,7 +15,6 @@ import { MAVEN_INTEL_SYSTEM_PROMPT, buildMavenIntelPrompt, } from '../../agent/mavenPrompts' -import { runAnalysisAgent } from '../../agent/runner' import { fetchPatch } from '../../clients/githubPatch' import { downloadAndExtractMavenSources } from '../../clients/mavenSourcesJar' import { @@ -141,7 +141,7 @@ export async function runIntelStageMaven( patches, ) - const agentResult = await runAnalysisAgent({ + const agentResult = await runClaudeAgentQuery({ prompt: agentPrompt, systemPrompt: MAVEN_INTEL_SYSTEM_PROMPT, cwd: pkgsrcDir, diff --git a/services/apps/packages_worker/src/blast-radius/stages/npm/intelNpm.ts b/services/apps/packages_worker/src/blast-radius/stages/npm/intelNpm.ts index 53b3e3f054..efd88b1ad8 100644 --- a/services/apps/packages_worker/src/blast-radius/stages/npm/intelNpm.ts +++ b/services/apps/packages_worker/src/blast-radius/stages/npm/intelNpm.ts @@ -2,6 +2,7 @@ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' +import { runClaudeAgentQuery } from '@crowd/anthropic-aws' import * as blastRadiusDal from '@crowd/data-access-layer/src/packages/blastRadius' import { findPackageId } from '@crowd/data-access-layer/src/packages/osv' import { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor' @@ -10,7 +11,6 @@ import { fetchPackument } from '../../../npm/fetchPackument' import { parseNpmName } from '../../../npm/normalize' import { isFetchError } from '../../../npm/types' import { INTEL_SCHEMA, INTEL_SYSTEM_PROMPT, buildIntelPrompt } from '../../agent/prompts' -import { runAnalysisAgent } from '../../agent/runner' import { fetchPatch } from '../../clients/githubPatch' import { downloadAndExtractTarball } from '../../clients/npmTarball' import { @@ -124,7 +124,7 @@ export async function runIntelStageNpm( patches, ) - const agentResult = await runAnalysisAgent({ + const agentResult = await runClaudeAgentQuery({ prompt: agentPrompt, systemPrompt: INTEL_SYSTEM_PROMPT, cwd: pkgsrcDir, diff --git a/services/apps/packages_worker/src/blast-radius/stages/nuget/intelNuGet.ts b/services/apps/packages_worker/src/blast-radius/stages/nuget/intelNuGet.ts index 33b2be5ba4..056dd9f1e6 100644 --- a/services/apps/packages_worker/src/blast-radius/stages/nuget/intelNuGet.ts +++ b/services/apps/packages_worker/src/blast-radius/stages/nuget/intelNuGet.ts @@ -2,6 +2,7 @@ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' +import { runClaudeAgentQuery } from '@crowd/anthropic-aws' import * as blastRadiusDal from '@crowd/data-access-layer/src/packages/blastRadius' import { getVersionNumbers } from '@crowd/data-access-layer/src/packages/blastRadiusDependents' import { findPackageId } from '@crowd/data-access-layer/src/packages/osv' @@ -14,7 +15,6 @@ import { NUGET_INTEL_SYSTEM_PROMPT, buildNuGetIntelPrompt, } from '../../agent/nugetPrompts' -import { runAnalysisAgent } from '../../agent/runner' import { fetchPatch } from '../../clients/githubPatch' import { downloadAndExtractNuGetSource } from '../../clients/nugetSource' import { @@ -135,7 +135,7 @@ export async function runIntelStageNuGet( patches, ) - const agentResult = await runAnalysisAgent({ + const agentResult = await runClaudeAgentQuery({ prompt: agentPrompt, systemPrompt: NUGET_INTEL_SYSTEM_PROMPT, cwd: pkgsrcDir, diff --git a/services/apps/packages_worker/src/blast-radius/stages/pypi/intelPyPi.ts b/services/apps/packages_worker/src/blast-radius/stages/pypi/intelPyPi.ts index 318b081427..1da6e9799b 100644 --- a/services/apps/packages_worker/src/blast-radius/stages/pypi/intelPyPi.ts +++ b/services/apps/packages_worker/src/blast-radius/stages/pypi/intelPyPi.ts @@ -2,6 +2,7 @@ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' +import { runClaudeAgentQuery } from '@crowd/anthropic-aws' import * as blastRadiusDal from '@crowd/data-access-layer/src/packages/blastRadius' import { getVersionNumbers } from '@crowd/data-access-layer/src/packages/blastRadiusDependents' import { findPackageIdByPurl } from '@crowd/data-access-layer/src/packages/osv' @@ -14,7 +15,6 @@ import { PYPI_INTEL_SYSTEM_PROMPT, buildPyPiIntelPrompt, } from '../../agent/pypiPrompts' -import { runAnalysisAgent } from '../../agent/runner' import { fetchPatch } from '../../clients/githubPatch' import { affectedEntriesForEcosystem, @@ -134,7 +134,7 @@ export async function runIntelStagePyPi( patches, ) - const agentResult = await runAnalysisAgent({ + const agentResult = await runClaudeAgentQuery({ prompt: agentPrompt, systemPrompt: PYPI_INTEL_SYSTEM_PROMPT, cwd: pkgsrcDir, diff --git a/services/apps/packages_worker/src/blast-radius/stages/reachabilityStage.ts b/services/apps/packages_worker/src/blast-radius/stages/reachabilityStage.ts index 6c50b98c07..f35fe07d77 100644 --- a/services/apps/packages_worker/src/blast-radius/stages/reachabilityStage.ts +++ b/services/apps/packages_worker/src/blast-radius/stages/reachabilityStage.ts @@ -3,12 +3,12 @@ import * as os from 'os' import * as path from 'path' import { promisify } from 'util' +import { runClaudeAgentQuery } from '@crowd/anthropic-aws' import * as blastRadiusDal from '@crowd/data-access-layer/src/packages/blastRadius' import { QueryExecutor } from '@crowd/data-access-layer/src/queryExecutor' import { toPromptSymbolSpec } from '../agent/promptKit' import { SymbolSpec } from '../agent/prompts' -import { runAnalysisAgent } from '../agent/runner' import { forEachWithConcurrency } from '../dependentsScan' const mkdtemp = promisify(fs.mkdtemp) @@ -139,7 +139,7 @@ export async function runReachabilityStage( try { const systemPrompt = cfg.buildSystemPrompt(spec) - const agentResult = await runAnalysisAgent({ + const agentResult = await runClaudeAgentQuery({ prompt: cfg.prompt, systemPrompt, cwd: depDir, diff --git a/services/apps/packages_worker/src/blast-radius/stages/rubygems/intelRubyGems.ts b/services/apps/packages_worker/src/blast-radius/stages/rubygems/intelRubyGems.ts index 919d7298df..73d4ac8b43 100644 --- a/services/apps/packages_worker/src/blast-radius/stages/rubygems/intelRubyGems.ts +++ b/services/apps/packages_worker/src/blast-radius/stages/rubygems/intelRubyGems.ts @@ -2,6 +2,7 @@ import * as fs from 'fs' import * as os from 'os' import * as path from 'path' +import { runClaudeAgentQuery } from '@crowd/anthropic-aws' import * as blastRadiusDal from '@crowd/data-access-layer/src/packages/blastRadius' import { getVersionNumbers } from '@crowd/data-access-layer/src/packages/blastRadiusDependents' import { findPackageId } from '@crowd/data-access-layer/src/packages/osv' @@ -14,7 +15,6 @@ import { RUBYGEMS_INTEL_SYSTEM_PROMPT, buildRubyGemsIntelPrompt, } from '../../agent/rubygemsPrompts' -import { runAnalysisAgent } from '../../agent/runner' import { fetchPatch } from '../../clients/githubPatch' import { affectedEntriesForEcosystem, @@ -136,7 +136,7 @@ export async function runIntelStageRubyGems( patches, ) - const agentResult = await runAnalysisAgent({ + const agentResult = await runClaudeAgentQuery({ prompt: agentPrompt, systemPrompt: RUBYGEMS_INTEL_SYSTEM_PROMPT, cwd: pkgsrcDir, diff --git a/services/apps/packages_worker/src/blast-radius/workflows.ts b/services/apps/packages_worker/src/blast-radius/workflows.ts index 1748310c4f..9543050f7b 100644 --- a/services/apps/packages_worker/src/blast-radius/workflows.ts +++ b/services/apps/packages_worker/src/blast-radius/workflows.ts @@ -11,7 +11,7 @@ const { blastRadiusStart, blastRadiusFail } = proxyActivities }) // Intel runs an Opus agent over the downloaded package source (up to 15 turns, -// 10-minute agent timeout in runAnalysisAgent) — give it headroom past that. +// 10-minute agent timeout in runClaudeAgentQuery) — give it headroom past that. const { blastRadiusIntel } = proxyActivities({ startToCloseTimeout: '20 minutes', heartbeatTimeout: '5 minutes', diff --git a/services/libs/anthropic-aws/package.json b/services/libs/anthropic-aws/package.json new file mode 100644 index 0000000000..711909d2dd --- /dev/null +++ b/services/libs/anthropic-aws/package.json @@ -0,0 +1,20 @@ +{ + "name": "@crowd/anthropic-aws", + "private": true, + "main": "src/index.ts", + "type": "commonjs", + "scripts": { + "lint": "npx eslint --ext .ts src --max-warnings=0", + "format": "npx prettier --write \"src/**/*.ts\"", + "format-check": "npx prettier --check .", + "tsc-check": "tsc --noEmit" + }, + "devDependencies": { + "@types/node": "^20.8.2", + "typescript": "^5.6.3" + }, + "dependencies": { + "@anthropic-ai/claude-agent-sdk": "^0.3.216", + "@crowd/logging": "workspace:*" + } +} diff --git a/services/apps/packages_worker/src/blast-radius/agent/runner.ts b/services/libs/anthropic-aws/src/agent.ts similarity index 50% rename from services/apps/packages_worker/src/blast-radius/agent/runner.ts rename to services/libs/anthropic-aws/src/agent.ts index 9aad7470ae..a9d845c79b 100644 --- a/services/apps/packages_worker/src/blast-radius/agent/runner.ts +++ b/services/libs/anthropic-aws/src/agent.ts @@ -1,10 +1,8 @@ -// @anthropic-ai/claude-agent-sdk ships ESM-only; packages_worker compiles to -// CommonJS, so it must be loaded via dynamic import rather than a static one. -// Agent runner wrapping Claude Agent SDK with read-only tool restrictions, -// API key fallback, structured output, and timeout support. import { getServiceChildLogger } from '@crowd/logging' -const log = getServiceChildLogger('blast-radius-agent-runner') +import { getAnthropicAwsAgentSdkEnv } from './credentials' + +const log = getServiceChildLogger('anthropic-aws-agent') export interface AgentRunResult { structuredOutput: Record | null @@ -14,7 +12,7 @@ export interface AgentRunResult { costUsd: number } -export interface RunAnalysisAgentInput { +export interface RunClaudeAgentQueryInput { prompt: string systemPrompt: string cwd: string @@ -22,13 +20,43 @@ export interface RunAnalysisAgentInput { schema: Record maxTurns?: number timeoutMs?: number - // Called on every streamed message (i.e. at least once per agent turn), so a - // caller can heartbeat a Temporal activity during a single up-to-timeoutMs - // agent call rather than only once the whole call returns. + // Read-only tools auto-allowed instead of bypassing permissions entirely. + // CLI refuses --dangerously-skip-permissions as root (container standard). + allowedTools?: string[] + disallowedTools?: string[] + // Called on every message to allow Temporal heartbeat without waiting for run completion. + // Enables activity to signal liveness during up-to-timeoutMs agent call. onProgress?: () => void } -export async function runAnalysisAgent(input: RunAnalysisAgentInput): Promise { +const DEFAULT_ALLOWED_TOOLS = ['Read', 'Grep', 'Glob'] +const DEFAULT_DISALLOWED_TOOLS = [ + 'Bash', + 'Write', + 'Edit', + 'NotebookEdit', + 'WebFetch', + 'WebSearch', + 'Task', +] + +function getErrorMessage( + isError: boolean, + result: string | undefined, + structuredOutput: Record | null, +): string { + if (isError) return result || 'Unknown error' + if (!structuredOutput) + return `Agent completed without structured output: ${result || 'no result text'}` + return '' +} + +// @anthropic-ai/claude-agent-sdk ships ESM-only; callers may compile to +// CommonJS, so dynamic import is required. Wraps SDK with read-only tool restrictions, +// AWS auth fallback, structured output, and timeout support. +export async function runClaudeAgentQuery( + input: RunClaudeAgentQueryInput, +): Promise { const { prompt, systemPrompt, @@ -37,34 +65,25 @@ export async function runAnalysisAgent(input: RunAnalysisAgentInput): Promise controller.abort(), timeoutMs) @@ -77,13 +96,9 @@ export async function runAnalysisAgent(input: RunAnalysisAgentInput): Promise { + return { + CLAUDE_CODE_USE_ANTHROPIC_AWS: '1', + ANTHROPIC_AWS_WORKSPACE_ID: credentials.workspaceId, + AWS_REGION: credentials.region, + ANTHROPIC_AWS_API_KEY: credentials.apiKey, + } +} diff --git a/services/libs/anthropic-aws/src/index.ts b/services/libs/anthropic-aws/src/index.ts new file mode 100644 index 0000000000..0f70577f51 --- /dev/null +++ b/services/libs/anthropic-aws/src/index.ts @@ -0,0 +1,2 @@ +export * from './credentials' +export * from './agent' diff --git a/services/libs/anthropic-aws/tsconfig.json b/services/libs/anthropic-aws/tsconfig.json new file mode 100644 index 0000000000..bf7f183850 --- /dev/null +++ b/services/libs/anthropic-aws/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../base.tsconfig.json", + "include": ["src/**/*"] +} diff --git a/services/libs/common/src/agentAuth.ts b/services/libs/common/src/agentAuth.ts deleted file mode 100644 index 425ba1cc53..0000000000 --- a/services/libs/common/src/agentAuth.ts +++ /dev/null @@ -1,97 +0,0 @@ -export type AgentAuthMode = 'bedrock' | 'anthropic-api-key' | 'cli-fallback' - -export interface AgentAuth { - mode: AgentAuthMode - env: Record | undefined - resolveModel(model: string): string -} - -export interface BedrockEnvVarNames { - accessKeyId: string - secretAccessKey: string - region: string -} - -export interface ResolveAgentAuthOptions { - apiKeyEnvVar?: string - modelBedrockMap?: Record - // Which env vars carry the Bedrock credential for this caller. There is no single - // org-wide Bedrock credential: CROWD_AWS_BEDROCK_* (the default) is the one shared by - // the enrichment workers, unrelated to Akrites. Callers under Akrites must pass their - // own AKRITES_* var names explicitly rather than relying on this default. - bedrockEnvVarNames?: BedrockEnvVarNames -} - -const DEFAULT_BEDROCK_ENV_VAR_NAMES: BedrockEnvVarNames = { - accessKeyId: 'CROWD_AWS_BEDROCK_ACCESS_KEY_ID', - secretAccessKey: 'CROWD_AWS_BEDROCK_SECRET_ACCESS_KEY', - region: 'CROWD_AWS_BEDROCK_REGION', -} - -const DEFAULT_BEDROCK_REGION = 'us-east-1' - -// The CLI prefers ANTHROPIC_API_KEY over Bedrock env vars when both are present in the -// subprocess env, so it must be stripped explicitly — otherwise Bedrock mode silently -// never activates even with valid AWS credentials. -function bedrockEnv(accessKeyId: string, secretAccessKey: string, region: string) { - const env = { ...process.env } as Record - delete env.ANTHROPIC_API_KEY - env.CLAUDE_CODE_USE_BEDROCK = '1' - env.AWS_ACCESS_KEY_ID = accessKeyId - env.AWS_SECRET_ACCESS_KEY = secretAccessKey - env.AWS_REGION = region - return env -} - -function resolveModelWith(modelBedrockMap: Record | undefined) { - return (model: string): string => { - if (!modelBedrockMap) { - return model - } - const resolved = modelBedrockMap[model] - if (!resolved) { - throw new Error(`No Bedrock model ID mapped for agent model "${model}"`) - } - return resolved - } -} - -export function resolveAgentAuth(opts: ResolveAgentAuthOptions = {}): AgentAuth { - const { - apiKeyEnvVar = 'BLAST_RADIUS_ANTHROPIC_API_KEY', - modelBedrockMap, - bedrockEnvVarNames = DEFAULT_BEDROCK_ENV_VAR_NAMES, - } = opts - - const bedrockAccessKeyId = process.env[bedrockEnvVarNames.accessKeyId] - const bedrockSecretAccessKey = process.env[bedrockEnvVarNames.secretAccessKey] - - if (bedrockAccessKeyId && bedrockSecretAccessKey) { - const region = process.env[bedrockEnvVarNames.region] || DEFAULT_BEDROCK_REGION - return { - mode: 'bedrock', - env: bedrockEnv(bedrockAccessKeyId, bedrockSecretAccessKey, region), - resolveModel: resolveModelWith(modelBedrockMap), - } - } - - const apiKey = process.env[apiKeyEnvVar] - if (apiKey) { - const baseUrl = process.env.BLAST_RADIUS_ANTHROPIC_BASE_URL - return { - mode: 'anthropic-api-key', - env: { - ...process.env, - ANTHROPIC_API_KEY: apiKey, - ...(baseUrl ? { ANTHROPIC_BASE_URL: baseUrl } : {}), - } as Record, - resolveModel: (model) => model, - } - } - - return { - mode: 'cli-fallback', - env: undefined, - resolveModel: (model) => model, - } -} diff --git a/services/libs/common/src/index.ts b/services/libs/common/src/index.ts index 968975a31f..6a4eb89884 100644 --- a/services/libs/common/src/index.ts +++ b/services/libs/common/src/index.ts @@ -21,7 +21,6 @@ import { export { getDbConstraint } from './errors/db' -export * from './agentAuth' export * from './env' export * from './timing' export * from './utils'