Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Copy to `.env` and fill in. `.env` is gitignored — never commit a filled-in copy.
#
# cp .env.example .env
#
# Used by the "Dry run (sdk auth)" VS Code launch config and `npm run dry-run:local`.
# The deployed worker reads these from the pod's env instead; see helm/values.yaml.

# --- Jira, through the self-hosted atlassian-write MCP server. Needs the corporate VPN. ---
MCP_ATLASSIAN_URL=https://atlassian-mcp-write.mapcolonies.net/sse
JIRA_BOT_ACCOUNT=developer-agent@mapcolonies.net
JIRA_BOT_DISPLAY_NAME=AGENT DEVELOPER

# --- Which account pays for the model. `sdk` reads CLAUDE_CODE_OAUTH_TOKEN, ---
# --- `api-key` reads ANTHROPIC_API_KEY. The worker refuses the other mode's credential. ---
MODEL_AUTH=sdk
# From `claude setup-token`. Bills, and is rate-limited as, the person whose token it is.
CLAUDE_CODE_OAUTH_TOKEN=
# Only for MODEL_AUTH=api-key. Leave empty in sdk mode.
ANTHROPIC_API_KEY=

# --- Repo lookups, the clone, the push and the pull request. `gh auth token` prints one. ---
GITHUB_TOKEN=

# --- Spend ceilings. Deliberately far below the defaults for a laptop run: ---
# --- MAX_TICKETS_PER_DAY is counted per process, so each dry run gets its own allowance. ---
MAX_TOKENS_PER_TICKET=150000
MAX_TURNS_PER_TICKET=15
MAX_TICKETS_PER_DAY=1
41 changes: 40 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,46 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
// One real cycle, stepped through. This is the whole pipeline: it claims a real
// `agent-ready` ticket, clones the repo its title names, hands it to the model, runs that
// repo's suite, and on a pass pushes an `agent/` branch and opens a real pull request.
//
// Needs the corporate VPN — the MCP server is not reachable from outside it — and a
// subscription token from `claude setup-token`, exported as CLAUDE_CODE_OAUTH_TOKEN in the
// shell VS Code was launched from. The ceilings below are deliberately far under the
// defaults: MAX_TICKETS_PER_DAY is counted per process, so this run gets its own allowance.
"type": "node",
"request": "launch",
"name": "Dry run (sdk auth)",
"runtimeExecutable": "npx",
"runtimeArgs": ["tsx", "--inspect-brk"],
"program": "${workspaceFolder}/src/dryRun.ts",
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"sourceMaps": true,
"smartStep": true,
"skipFiles": ["${workspaceFolder}/node_modules/**/*.js", "<node_internals>/**/*.js"],
// Everything the run needs lives in `.env` (gitignored). Copy `.env.example` to
// `.env` and fill in the two tokens. Nothing has to be exported into the shell, so
// VS Code no longer has to be launched from a particular terminal.
"envFile": "${workspaceFolder}/.env"
},
{
// The same seam with every edge faked — no Jira, no GitHub, no model, no money. The place
// to find out where the breakpoints want to go before spending a real ticket on it.
"type": "node",
"request": "launch",
"name": "Integration: one cycle (fakes)",
"runtimeExecutable": "npx",
"runtimeArgs": ["vitest", "run", "--project", "integration", "--no-file-parallelism", "--coverage.enabled=false"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"autoAttachChildProcesses": true,
"sourceMaps": true,
"smartStep": true,
"skipFiles": ["<node_internals>/**/*.js"]
},
{
"type": "node",
"request": "launch",
Expand All @@ -27,7 +67,6 @@
"TELEMETRY_METRICS_ENABLED": "false",
"TELEMETRY_TRACING_URL": "http://localhost:55681/v1/trace",
"TELEMETRY_METRICS_URL": "http://localhost:55681/v1/metrics"
// "OPENAPI_FILE_PATH": "./openapi3.yaml"
}
}
]
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ RUN npm run build

FROM node:24.10.0-alpine3.22 AS production

RUN apk add dumb-init
# `git` because the worker clones the repository a ticket names and then commits and pushes to
# it — that is worker code, not model tooling: the Agent SDK is given file and test tools only
# and has no shell to reach a binary with (see MODEL_TOOLS in src/agent/sdkOptions.ts).
RUN apk add --no-cache dumb-init git

ENV NODE_ENV=production

Expand Down
199 changes: 181 additions & 18 deletions README.md

Large diffs are not rendered by default.

55 changes: 51 additions & 4 deletions helm/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,18 @@ spec:
imagePullPolicy: {{ .pullPolicy | default "IfNotPresent" }}
{{- end }}
{{- if .Values.command }}
command:
command:
{{- toYaml .Values.command | nindent 12 }}
{{- if .Values.args }}
args:
args:
{{- toYaml .Values.args | nindent 12 }}
{{- end }}
{{- end }}
volumeMounts:
# The per-ticket clones. Writable because the worker clones a repository and runs its
# own `npm ci` and test suite in here; nothing in it outlives a ticket.
- mountPath: {{ .Values.worker.workspaceRoot | quote }}
name: workspace
{{- if .Values.caSecretName }}
- mountPath: {{ printf "%s/%s" .Values.caPath .Values.caKey | quote }}
name: root-ca
Expand All @@ -83,6 +87,46 @@ spec:
value: {{ .Values.worker.maxTicketsPerRun | quote }}
- name: MAX_CONCURRENT_TICKETS
value: {{ .Values.worker.maxConcurrentTickets | quote }}
- name: WORKSPACE_ROOT
value: {{ .Values.worker.workspaceRoot | quote }}
- name: MAX_TOKENS_PER_TICKET
value: {{ .Values.worker.maxTokensPerTicket | quote }}
- name: MAX_TURNS_PER_TICKET
value: {{ .Values.worker.maxTurnsPerTicket | quote }}
- name: MAX_TICKETS_PER_DAY
value: {{ .Values.worker.maxTicketsPerDay | quote }}
- name: AGENT_GIT_NAME
value: {{ .Values.worker.gitName | quote }}
- name: AGENT_GIT_EMAIL
value: {{ .Values.worker.gitEmail | quote }}
{{- if .Values.worker.model }}
- name: AGENT_MODEL
value: {{ .Values.worker.model | quote }}
{{- end }}
# Which account the model calls are billed to, stated rather than inferred. The
# worker refuses to start on a credential its mode did not ask for, so this value
# and the Secret's key below have to agree.
- name: MODEL_AUTH
value: {{ .Values.worker.modelAuth | quote }}
{{- if .Values.worker.anthropicSecretName }}
# The one credential the worker talks to the model with. Required: it refuses to
# start without it rather than claiming a ticket and finding out — see
# readModelCredential. The variable name is the mode's, never both at once.
- name: {{ if eq .Values.worker.modelAuth "sdk" }}CLAUDE_CODE_OAUTH_TOKEN{{ else }}ANTHROPIC_API_KEY{{ end }}
valueFrom:
secretKeyRef:
name: {{ .Values.worker.anthropicSecretName | quote }}
key: {{ .Values.worker.anthropicSecretKey | quote }}
{{- end }}
{{- if .Values.worker.githubSecretName }}
# Repo lookups, the clone, the push and the pull request. MAPCO-11428 replaces this
# with an App installation token minted per run.
- name: GITHUB_TOKEN
valueFrom:
secretKeyRef:
name: {{ .Values.worker.githubSecretName | quote }}
key: {{ .Values.worker.githubSecretKey | quote }}
{{- end }}
{{- if .Values.caSecretName }}
- name: REQUESTS_CA_BUNDLE
value: {{ printf "%s/%s" .Values.caPath .Values.caKey | quote }}
Expand All @@ -91,7 +135,7 @@ spec:
{{- end }}
{{- if .Values.extraEnvVars }}
{{- toYaml .Values.extraEnvVars | nindent 12 }}
{{- end }}
{{- end }}
envFrom:
- configMapRef:
name: {{ include "developer-agent-bot.fullname" . }}
Expand All @@ -103,12 +147,15 @@ spec:
{{ tpl (toYaml .Values.sidecars) . | nindent 8 }}
{{- end }}
volumes:
- name: workspace
emptyDir:
sizeLimit: {{ .Values.worker.workspaceSizeLimit | quote }}
{{- if .Values.caSecretName }}
- name: root-ca
secret:
secretName: {{ .Values.caSecretName }}
{{- end }}
{{- if .Values.extraVolumes -}}
{{ tpl (toYaml .Values.extraVolumes) . | nindent 8 }}
{{- end }}
{{- end }}
{{- end -}}
65 changes: 60 additions & 5 deletions helm/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ global:
metrics: {}

mclabels:
component: worker
# Must be one of the values mclabels' values.schema.json allows:
# frontend | backend | database | proxy-server | cache-server | infrastructure.
# There is no "worker" member — this outbound-only poller is a backend process.
component: backend
partOf: developer-agent-bot
owner: common
prometheus:
Expand Down Expand Up @@ -69,6 +72,55 @@ worker:
maxTicketsPerRun: 1
maxConcurrentTickets: 1

# Where the per-ticket clones are made. A writable emptyDir, because the worker now clones a
# repository and runs its `npm ci` and its suite, and nothing on disk outlives a ticket.
workspaceRoot: '/workspace'
# Room for one clone plus its node_modules, times maxConcurrentTickets, with headroom. A
# MapColonies service's node_modules is comfortably over 1Gi.
workspaceSizeLimit: 8Gi

# Spend ceilings (MAPCO-11435). Conservative on purpose: a metered API key has no ceiling of
# its own, so these are the only thing between a stuck agent and an invoice. There is
# deliberately no value meaning "unlimited" — a ceiling that can be switched off gets switched
# off during an incident and stays off.
maxTokensPerTicket: 200000
maxTurnsPerTicket: 40
maxTicketsPerDay: 5

# Who the worker's commits are authored by. The real answer belongs to the GitHub App
# (MAPCO-11428): `{app-slug}[bot]` and `{app-id}+{app-slug}[bot]@users.noreply.github.com`.
# Until then this is a valid noreply address that is recognisably not a person's.
gitName: 'mapcolonies-developer-agent[bot]'
gitEmail: 'mapcolonies-developer-agent[bot]@users.noreply.github.com'

# Model override. Empty means the worker's own default (agent/sdkOptions.ts).
model: ''

# Which account the model calls are billed to. `sdk` reads CLAUDE_CODE_OAUTH_TOKEN — a Claude
# subscription token from `claude setup-token`; `api-key` reads ANTHROPIC_API_KEY instead.
# Explicit configuration, never inference: the two look alike to the SDK and bill completely
# differently, and a worker given the other mode's credential refuses to start rather than
# quietly billing an account nobody chose.
#
# `sdk` bills, and is rate-limited as, the person whose token it is. Anthropic's Agent SDK
# documentation states that claude.ai login and its rate limits may not be used for products
# built on the Agent SDK unless previously approved — running in this mode asserts that this
# deployment has that approval. The worker and that person's own interactive use also share one
# quota, and the pod crash-loops when the token expires.
modelAuth: 'sdk'

# The model credential. **Required** — the worker refuses to start without it, which is the
# loudest thing a missing Secret can be. The key must hold the credential the mode above asks
# for: a subscription token for `sdk`, an API key for `api-key`.
anthropicSecretName: ''
anthropicSecretKey: 'oauthToken'

# The GitHub credential, used to look up repos, clone private ones, push and open the pull
# request. Interim: MAPCO-11428 replaces this with App installation tokens minted per run, so
# that there is no long-lived secret here at all.
githubSecretName: ''
githubSecretKey: 'token'

env:
logLevel: info
logPrettyPrintEnabled: false
Expand All @@ -79,13 +131,16 @@ env:
enabled: false
url: http://localhost:55681/v1/metrics

# Sized for what the worker actually does now: clone a repository, run its `npm ci` and then its
# test suite, next to an Agent SDK session. The previous 100m/128Mi was right for a pod that only
# polled Jira and would OOM-kill on the first `npm ci`.
resources:
enabled: true
value:
limits:
cpu: 100m
memory: 128Mi
cpu: '2'
memory: 4Gi
requests:
cpu: 100m
memory: 128Mi
cpu: 500m
memory: 1Gi

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@
"assets:copy": "copyfiles -f ./config/* ./dist/config && copyfiles ./package.json dist",
"clean": "rimraf dist",
"prepare": "node .husky/install.mjs",
"dry-run": "tsx src/dryRun.ts"
"dry-run": "tsx src/dryRun.ts",
"dry-run:local": "tsx --env-file=.env src/dryRun.ts",
"preflight": "tsx --env-file=.env src/preflight.ts"
},
"directories": {
"test": "tests"
Expand Down
84 changes: 0 additions & 84 deletions src/agent/apiKey.ts

This file was deleted.

Loading
Loading