A TypeScript-based Express.js server powering the Evergreen AI Service.
- Node.js v22 or higher
- Yarn package manager
- MongoDB instance installed and running
- Azure OpenAI key
Non-secret variables are tracked in .env.defaults, with environment-specific variables in .env.<NODE_ENV> files.
Update .env.local or .env.<NODE_ENV>.local files with secrets for external services. These files are ignored by git. Refer to the team password manager or ask a teammate for credentials.
The ENCRYPTION_KEY environment variable is required for encrypting sensitive data (e.g., user API keys) stored in MongoDB. It must be a 32-byte hex string (64 characters) for AES-256 encryption.
Generating a new key:
openssl rand -hex 32Storage: In deployed environments, the encryption key is stored in Kanopy secrets. A default key is provided in .env.defaults for local development.
-
Clone the repository or navigate to the project directory.
-
Install dependencies:
yarn install
sage/
├── src/
│ ├── api-server/
│ │ ├── index.ts # API server setup
│ │ ├── middlewares/ # Express middlewares
│ │ │ ├── index.ts
│ │ │ └── logging.ts
│ │ ├── routes/ # HTTP route handlers
│ │ │ ├── completions/
│ │ │ │ ├── index.ts
│ │ │ │ └── parsley.ts
│ │ │ ├── health.ts
│ │ │ ├── index.ts
│ │ │ └── root.ts
│ │ └── types/
│ │ └── index.ts
│ ├── config/
│ │ └── index.ts # Environment config
│ ├── db/
│ │ └── connection.ts # MongoDB connection
│ ├── mastra/ # Mastra agent framework
│ │ ├── agents/
│ │ │ └── evergreenAgent.ts
│ │ ├── tools/
│ │ │ └── some_tool.ts # [Tools documentation](https://mastra.ai/en/docs/tools-mcp/overview)
│ │ ├── workflows/
│ │ │ └── some_workflow.ts # [Workflows documentation](https://mastra.ai/en/docs/workflows/overview)
│ │ ├── models/
│ │ │ └── openAI/
│ │ │ ├── baseModel.ts
│ │ │ └── gpt41.ts
│ │ └── index.ts # Mastra setup/exports
│ ├── types/
│ │ └── index.ts
│ ├── utils/
│ │ ├── logger/
│ │ │ ├── index.ts
│ │ │ ├── setup.ts
│ │ │ ├── winstonMastraLogger.ts
│ │ │ └── logger.test.ts
│ │ └── index.ts
│ ├── __tests__/ # Unit and integration tests
│ └── main.ts # App entry point
├── environments/
│ └── staging.yaml # Deployment configuration
├── scripts/ # Project automation scripts
├── .drone.yml # Drone CI pipeline
├── .evergreen.yml # Evergreen configuration
├── .env.defaults # Shared environment variables
├── .env.<NODE_ENV> # Non-secret environment variables
└── README.md
yarn devStarts the server using vite-node, with hot-reloading and TypeScript support. Default port: 8080 (or set via the PORT environment variable).
yarn build
yarn startCompiles the TypeScript code and starts the production server using Node.js.
yarn cleanRemoves the dist/ directory.
Most API endpoints require authentication via the x-kanopy-internal-authorization header. For local development, there are two ways to authenticate requests:
To test the Cursor agent integration locally, you'll need to:
-
Get a Cursor API Key: Obtain an API key from the Cursor Dashboard. Navigate to the API Keys section and generate a new key.
-
Add the Key via REST Endpoints: Use the local API endpoints to store your key:
POST /pr-bot/user/cursor-key- Create or update your Cursor API keyGET /pr-bot/user/cursor-key- Check if a key is registeredDELETE /pr-bot/user/cursor-key- Remove your stored key
Example:
curl -X POST http://localhost:8080/pr-bot/user/cursor-key \ -H "Content-Type: application/json" \ -d '{"apiKey": "your-cursor-api-key"}'
When the USER_NAME environment variable is set, the authentication middleware uses it as the user ID, bypassing JWT validation. Add it to your .env.local:
USER_NAME=your.email@mongodb.comThen test endpoints with simple curl commands:
curl -X POST http://localhost:8080/pr-bot/user/cursor-key \
-H "Content-Type: application/json" \
-d '{"apiKey": "your-api-key-here"}'If USER_NAME is not set, you can pass a JWT in the x-kanopy-internal-authorization header. The middleware only decodes the payload (doesn't verify the signature), so you can construct a test JWT:
# JWT payload: {"sub":"test@example.com"}
curl -X POST http://localhost:8080/pr-bot/user/cursor-key \
-H "Content-Type: application/json" \
-H "x-kanopy-internal-authorization: eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0QGV4YW1wbGUuY29tIn0.fake" \
-d '{"apiKey": "your-api-key-here"}'To create a custom test JWT, base64-encode your desired payload:
echo -n '{"sub":"your.email@mongodb.com"}' | base64
# Use the output as the middle segment: header.PAYLOAD.signature-
Docker contexts now respect
.dockerignore, so localdocker buildanddocker buildxcommands skip large directories such asnode_modules/,coverage/, and generated GraphQL schemas. If you need to reference a file that is ignored by default, build with--no-cacheor temporarily remove the entry. -
The Drone
publishstep uses Kaniko layer caching backed by ECR. Subsequent CI builds reuse previously published layers automatically, so rebuilds after small changes are significantly faster.
You can opt into the same cache when iterating locally with BuildKit:
-
Authenticate against the Sage ECR registry (example):
aws ecr get-login-password --region us-east-1 \ | docker login --username AWS --password-stdin 795250896452.dkr.ecr.us-east-1.amazonaws.com -
Run
docker buildx buildwith the cache image that Drone maintains:docker buildx build \ --platform linux/arm64 \ --cache-from type=registry,ref=795250896452.dkr.ecr.us-east-1.amazonaws.com/devprod-evergreen/${DRONE_REPO_NAME}-cache \ --cache-to type=registry,ref=795250896452.dkr.ecr.us-east-1.amazonaws.com/devprod-evergreen/${DRONE_REPO_NAME}-cache,mode=max \ -t sage:local .
Replace ${DRONE_REPO_NAME} with your repository name if you are building from a fork (Sage uses sage). The -t sage:local tag is just a local image label—name it however you like. The --cache-to flag updates the shared cache so that your next build—and CI—can reuse the warmed layers.
The project uses Mastra, a framework for building agentic systems with tools and workflows.
yarn mastra:devLaunches a local Mastra server at http://localhost:4111 for agent testing.
- Agents: Add or update agents in
src/mastra/agents. - Tools: Place reusable tools in
src/mastra/tools. Tools are composable functions an agent can call. - Workflows: Add workflows to
src/mastra/workflows. Workflows define multi-step logic that agents can follow.
All agents and workflows should be registered in src/mastra/index.ts.
Sage relies on Evergreen’s GraphQL schema for both query linting and type
generation. To keep the schema in sync with Evergreen, create a local symlink
to the Evergreen repository’s graphql/schema directory.
Run the following command from the root of the Sage repository, replacing
<path_to_evergreen_repo> with the absolute path to your local Evergreen
checkout:
ln -s <path_to_evergreen_repo>/graphql/schema sdlschemaThis creates a folder-level symlink named sdlschema/ that Sage’s ESLint and
GraphQL Code Generator will pick up automatically.
With the schema symlinked, ESLint will validate your .ts, .gql, and
.graphql files against the Evergreen schema during development. You can run a
manual lint pass at any time with:
yarn lintWe use @graphql-codegen to generate
TypeScript types for queries, mutations, and their variables. The generated
types live in src/gql/generated/types.ts.
Run the generator after editing or adding GraphQL operations:
yarn codegenIf the schema or your operations change, re-run yarn codegen to keep the
types up to date. The command will also run Prettier on the generated file.
- If ESLint or codegen cannot find the schema, verify the
sdlschemasymlink path and that the Evergreen repository is on the expected branch. - If dependencies appear out of date, try
yarn installoryarn cleanfollowed byyarn installto refreshnode_modules.
We use evals to measure model performance through the Braintrust platform.
For detailed information about running evals, managing datasets, scoring, and reporting, see the Evals documentation.
Before deploying, you can check which commits are pending deployment to an environment:
-
Switch to the appropriate kubectl context:
- Production: Run
kcp(switches to production context) - Staging: Run
kcs(switches to staging context)
- Production: Run
-
Check pending commits:
yarn pending-commits
For JSON output:
yarn pending-commits:json
This will show all commits between what is currently deployed and your local HEAD, including commit hashes, messages, and GitHub URLs.
Before pushing to staging, drop a note in 🔒evergreen-ai-devs to make sure no one is using it.
Drone can promote builds opened on PRs to staging. Before starting, install and configure the Drone CLI.
- Open a PR with your changes (a draft is okay). This will kick off the
publishstep. - Check pending commits using
kcs && yarn pending-commitsto see what will be deployed. - Once the build completes, find the build number on Drone.
- Promote the build to staging:
- CLI: Run
drone build promote evergreen-ci/sage <DRONE_BUILD_NUMBER> staging - Web UI: Click
…>Promoteon your build's page. Enter "staging" in the "Target" field and submit.
- CLI: Run
Local deploys are slower but useful. First install Rancher Desktop as your container manager. Open Rancher and then run yarn deploy:staging from Sage to kick off the deploy.
Note that Drone's deployments page will not reflect local deploys. To verify your deploy has been pushed, install Helm and run helm status sage.
To deploy to production:
- Check pending commits:
kcp && yarn pending-commits - Find the build on Drone for the commit you want to deploy (must be on
mainbranch). - Promote the build to production:
- CLI: Run
drone build promote evergreen-ci/sage <DRONE_BUILD_NUMBER> production - Web UI: Click
…>Promoteon your build's page. Enter "production" in the "Target" field and submit.
- CLI: Run
Note: You must be promoting a Drone build that pushed a commit to main.