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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# apollo

## 2.0.0

### Major Changes

- 782b846: Force authorisation on all API keys

## 1.5.0

### Minor Changes
Expand Down
30 changes: 19 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ code in this repository.
**Never chain Bash commands with `&&`, `;`, or `cd ... &&`. Use separate Bash
calls instead.**

**Do not end single-sentence console/log output strings with a full stop.**
`console.log("rejected request")` not `console.log("rejected request.")`

## Commands

### Development
Expand Down Expand Up @@ -46,7 +49,7 @@ TypeScript) service modules.
any `services/<name>/` directory not starting with `_`. Detects service type
by checking for `<name>.py` (Python) or `<name>.ts` (TypeScript) index file.
- **Instance auth** (`platform/src/auth/`): `/services/*` uses a
map-if-known-else-forward auth hook that is always active (no flag). The auth surface
known-client-only auth hook that is always active (no flag). The auth surface
is split into three named concerns: the client-credential authenticate hook and Anthropic-key
resolver on the injectable `InstanceAuth` class (`instance-auth.ts`), the
internal-call exemption (`internal-token.ts`), and the shared `hashToken`
Expand All @@ -57,13 +60,19 @@ TypeScript) service modules.
`POSTGRES_URL` when unset, so local dev needs only the one var; staging/prod point
`APOLLO_CLIENTS_DB_URL` at a separate credentials DB). The inbound `api_key` is
treated purely as a credential and is **never** forwarded to the LLM on a known
match: it is replaced with the matched client's stored `anthropic_api_key`, or
stripped (falling back to the global `ANTHROPIC_API_KEY`) when that column is
`NULL`. An *unknown* key is forwarded unchanged only if it is `sk-ant-`-shaped
(bring-your-own key); an unknown non-`sk-ant-` key is rejected with `401`
(likely a Lightning credential that must not leak to the LLM). No `api_key`
falls back to the global key. The resolver (`InstanceAuth.resolveKey`) returns a
tagged `KeyResolution` (`useKey`/`useGlobal`/`forward`/`passthrough`) dispatched
match: it is replaced with the matched client's stored `anthropic_api_key`. A
known client must have a stored key; a `NULL` `anthropic_api_key` is a
server-side misconfiguration and is rejected with `500` (`CLIENT_MISCONFIGURED`,
reported to Sentry via `captureException`), never billed to the global key. An
*unknown* key is **rejected** regardless of shape: `401` when the
lookup completes and confirms no such client (a verified unknown that must not
leak to the LLM), and `503` when the
client store can't be reached (we can't verify, so don't guess — retryable,
never a misleading `401`). A request with no `api_key` at all is served by the
global `ANTHROPIC_API_KEY` when one is configured (the field is dropped); with no
global key to serve it, the keyless request is rejected with `401`. The resolver
(`InstanceAuth.resolveKey`) returns a
tagged `KeyResolution` (`useKey`/`useGlobal`/`passthrough`) dispatched
by a named switch in `services.ts`. The stored
`anthropic_api_key` may be plaintext or AES-256-GCM-encrypted (`enc:v1:`
values, decrypted with `APOLLO_ENC_KEY`; see
Expand All @@ -74,9 +83,8 @@ TypeScript) service modules.
stale-while-revalidate, so a burst of requests at the TTL boundary shares one
DB read (cold start awaits it; a warm-but-stale cache is served while one
background refresh runs) rather than stampeding the DB. If the table can't be
reached, known-client swaps don't resolve and callers degrade to the
shape-checked forward path (the same `sk-ant-` rule applies; it does not
blanket-reject). The auth hook is scoped to
reached, known-client swaps don't resolve and a caller with an `api_key` gets a
retryable `503` (we can't verify them). The auth hook is scoped to
`/services/*`, so health/root endpoints outside that group are unaffected.
Internal Apollo-to-Apollo `apollo()` calls are exempt via a per-process
internal token (`APOLLO_INTERNAL_TOKEN`, minted at startup; `bridge.ts` injects
Expand Down
128 changes: 37 additions & 91 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,18 @@ This repo contains:
This README covers running, debugging and deploying the server. Deeper docs live
alongside the code:

- **Architecture** see [Server Architecture](#server-architecture) below, and
- **Architecture** - see [Server Architecture](#server-architecture) below, and
each service's own README for service-specific detail.
- **Contributing & service conventions** [`CONTRIBUTING.md`](CONTRIBUTING.md)
- **Contributing & service conventions** - [`CONTRIBUTING.md`](CONTRIBUTING.md)
explains how to add and structure a Python service (entry.py, imports,
logging, code quality).
- **Services** every service has its own README with payload specs and
- **Services** - every service has its own README with payload specs and
examples. See the [Services](#services) index below.
- **Instance auth** — [`platform/src/auth/README.md`](platform/src/auth/README.md)
covers authenticating `/services/*` per client and managing per-client Anthropic keys.
- **Testing** — [`services/testing/README.md`](services/testing/README.md)
- **Instance auth** -
[`platform/src/auth/README.md`](platform/src/auth/README.md) covers
authenticating `/services/*` per client and managing per-client Anthropic
keys.
- **Testing** - [`services/testing/README.md`](services/testing/README.md)
documents the shared acceptance-test harness (YAML specs + LLM-as-judge);
per-service guides live in each service's `tests/`.

Expand Down Expand Up @@ -213,53 +215,11 @@ the [Contribution Guide](CONTRIBUTING.md) for details.
## Services

Each service lives in `services/<name>/` and is auto-mounted by service
discovery. Every mounted service has its own README with payload specs and
examples — start there for anything service-specific.

### Chat & orchestration

| Service | What it does |
| --- | --- |
| [`global_chat`](services/global_chat/README.md) | Orchestrator and single entry point for OpenFn AI chat; routes to subagents or escalates to a planner. Also see its [`PAYLOAD_SPEC.md`](services/global_chat/PAYLOAD_SPEC.md). |
| [`job_chat`](services/job_chat/README.md) | AI chat for OpenFn job code, with a code-suggestions/auto-patch mode. |
| [`workflow_chat`](services/workflow_chat/README.md) | Generates and edits OpenFn workflow YAML, preserving job code and IDs. |
| [`doc_agent_chat`](services/doc_agent_chat/README.md) | Agentic chat over a project's uploaded documents (RAG). |

### Docs, search & RAG

| Service | What it does |
| --- | --- |
| [`search_docsite`](services/search_docsite/README.md) | Semantic search over the OpenFn docs (`docsite` Pinecone index). |
| [`embed_docsite`](services/embed_docsite/README.md) | Downloads and indexes the OpenFn docs into the `docsite` index. |
| [`doc_agent_upload`](services/doc_agent_upload/README.md) | Fetches and indexes project documents into the `doc-agent` index. |

### Adaptors

| Service | What it does |
| --- | --- |
| [`load_adaptor_docs`](services/load_adaptor_docs/README.md) | Parses adaptor function docs into Postgres. |
| [`search_adaptor_docs`](services/search_adaptor_docs/README.md) | Queries adaptor docs back out of Postgres by version. |
| [`latest_adaptors`](services/latest_adaptors/README.md) | Fetches the latest adaptor versions from the OpenFn repo. |
| [`adaptor_apis`](services/adaptor_apis/README.md) | **TypeScript** service: produces a JSON schema of an adaptor's API. |

### Medical vocab & embeddings

| Service | What it does |
| --- | --- |
| `vocab_mapper` | Maps medical vocabularies (LOINC/SNOMED) against the `apollo-mappings` index. (No README yet.) |
| [`embeddings`](services/embeddings/README.md) | Vector-store wrapper used by the vocab services. |
| [`embed_loinc_dataset`](services/embed_loinc_dataset/README.md) | Embeds the LOINC dataset into `apollo-mappings`. |
| [`embed_snomed_dataset`](services/embed_snomed_dataset/README.md) | Embeds the SNOMED dataset into `apollo-mappings`. |
| [`embeddings_demo`](services/embeddings_demo/README.md) | Standalone embeddings demo (Zilliz). |

### Utilities & support

| Service | What it does |
| --- | --- |
| [`status`](services/status/README.md) | Health check: validates Anthropic, OpenAI and Pinecone keys. |
| [`echo`](services/echo/README.md) | Test service that returns its input; useful for verifying the pipeline. |
| [`auth`](platform/src/auth/README.md) | Instance-auth hook + provisioning (server layer, under `platform/`, not a mounted service). See [Database](#database). |
| [`testing`](services/testing/README.md) | Shared acceptance-test harness (not a mounted service). |
discovery.

Start the server with `bun start` and head to `http://localhost:3000` to see an
overview of active services. Or go to `https://apollo.openfn.org` to see the
production services.

## Database

Expand All @@ -272,12 +232,12 @@ apply with `psql`, both written with `CREATE TABLE IF NOT EXISTS` so re-running
them is safe:

- [`services/load_adaptor_docs/schema.sql`](services/load_adaptor_docs/schema.sql)
`adaptor_function_docs`. This table is also created lazily the first time
`load_adaptor_docs` runs, so applying it by hand is optional.
- `lightning_clients` created and kept current by the migration runner
- `adaptor_function_docs`. This table is also created lazily the first time
`load_adaptor_docs` runs, so applying it by hand is optional.
- `lightning_clients` - created and kept current by the migration runner
(`platform/src/db/migrate.ts`, migrations under
[`platform/migrations/`](platform/migrations/)). It is applied automatically at
Apollo startup when `POSTGRES_URL` is set; no manual `psql` step is needed.
[`platform/migrations/`](platform/migrations/)). It is applied automatically
at Apollo startup when `POSTGRES_URL` is set; no manual `psql` step is needed.

First, make sure you've configured your desired `POSTGRES_URL` in your `.env`
file.
Expand All @@ -299,47 +259,33 @@ Create a Postgres DB matching your POSTGRES_URL from the `.env` file

### Instance authentication (optional)

`/services/*` can be authenticated so that only known clients (e.g. specific Lightning
instances) may call it, with Apollo using **each client's own Anthropic API
key** for that client's requests.

- It is **transparent and backward compatible** (map-if-known-else-forward): the
auth hook is always active but only swaps in a key when it recognises the caller.
Clients are looked up in the `lightning_clients` table via `POSTGRES_URL`; if
that table can't be reached, known-client swaps simply don't resolve and every
caller degrades to the forward path (it does **not** blanket-reject).
- The credential is the **`api_key` the caller already sends in the request
body** — there is no bearer token, no `Authorization` header, and no
Lightning-side change. Apollo stores only a SHA-256 hash of it.
- On a match, the inbound `api_key` is treated purely as a credential and is
**never** forwarded to the LLM: it is replaced with the client's stored
Anthropic key (so LLM usage bills to that client), or stripped — falling back
to the global `ANTHROPIC_API_KEY` — if the client has no stored key.
- An **unrecognised** key is forwarded unchanged **only if it looks like an
Anthropic key** (prefix `sk-ant-`) — this is the bring-your-own-key path. An
unrecognised key that is _not_ `sk-ant-`-shaped is a likely Lightning
credential, so it is **rejected** (`401`) rather than forwarded, which would
leak it to the LLM. A request with no `api_key` falls back to the global key.
- Health/root endpoints (`/livez`, `/status`, `/`) are outside `/services/*` and
never subject to the auth hook. Internal Apollo-to-Apollo `apollo()` calls are exempt via a
per-process internal token (`APOLLO_INTERNAL_TOKEN`), not by network position.
`/services/*` is authenticated so that only known clients (e.g. specific
Lightning instances) may call it, with Apollo using **each client's own
Anthropic API key** for that client's requests.

The auth hook is always active. A request that carries an `api_key` must resolve
to a known Lightning client or it is **rejected**. Clients are looked up in the
`lightning_clients` table via `APOLLO_CLIENTS_DB_URL` (falling back to
`POSTGRES_URL` when unset).

To enable it and provision clients, see
[`platform/src/auth/`](platform/src/auth/README.md).

#### Deploying: pin `APOLLO_INTERNAL_TOKEN` in production

`APOLLO_INTERNAL_TOKEN` is the mechanism that lets internal `apollo()` self-calls
through the auth hook: a self-call carries it in the `x-apollo-internal` header and the
hook matches it. Because the global `ANTHROPIC_API_KEY` is dev-only, a token that
fails to match is a dead end (a `401`), not a soft fallback to the global key — so
the match has to work in every topology.
`APOLLO_INTERNAL_TOKEN` is the mechanism that lets internal `apollo()`
self-calls through the auth hook: a self-call carries it in the
`x-apollo-internal` header and the hook matches it. Because the global
`ANTHROPIC_API_KEY` is dev-only, a token that fails to match is a dead end (a
`401`), not a soft fallback to the global key - so the match has to work in
every topology.

- **Always set `APOLLO_INTERNAL_TOKEN` to a shared value across the deployment in
production.** When it is set, the per-process minting path never runs.
- **Always set `APOLLO_INTERNAL_TOKEN` to a shared value across the deployment
in production.** When it is set, the per-process minting path never runs.
- The per-process random mint is a **dev-only convenience**. Apollo assumes one
Bun process per host; the `apollo()` self-call relies on loopback calls landing
on the same process, so a minted token only works single-process-per-host.
Bun process per host; the `apollo()` self-call relies on loopback calls
landing on the same process, so a minted token only works
single-process-per-host.
- If `reusePort` clustering is ever enabled, a shared `APOLLO_INTERNAL_TOKEN` is
**required**, not optional: a self-call can otherwise be routed to a sibling
process that minted a different token and will `401`. Startup logs the token's
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "apollo",
"module": "platform/index.ts",
"version": "1.5.0",
"version": "2.0.0",
"type": "module",
"scripts": {
"start": "NODE_ENV=production bun platform/src/index.ts",
Expand Down
Loading