Merge commit 'b2ef19bbf63fd0c9f9601e1d197c6fd58c0f77dc'

This commit is contained in:
Alexa Amundson
2025-11-25 14:05:48 -06:00
6 changed files with 525 additions and 0 deletions

11
.env.example Normal file
View File

@@ -0,0 +1,11 @@
OS_ROOT=https://blackroad.systems
SERVICE_BASE_URL=https://api.blackroad.systems
CORE_BASE_URL=https://core.blackroad.systems
OPERATOR_BASE_URL=https://operator.blackroad.systems
LOG_LEVEL=info
PORT=8080
# Version info for /version endpoint (optional - will use package.json or defaults)
# BR_OS_API_VERSION=0.2.0
# BR_OS_API_COMMIT=abc123
# BR_OS_ENV=local

283
.github/copilot-instructions.md vendored Normal file
View File

@@ -0,0 +1,283 @@
# System Prompt for blackroad-os-api
You are an AI engineer working **inside this repository**: `blackroad-os-api` in the BlackRoad OS ecosystem.
Your job:
- Maintain a **small, clean, well-typed API service**.
- Expose **stable, boring, predictable endpoints** that other BlackRoad OS services and agents can rely on.
- Follow shared BlackRoad OS conventions for **health/version/ready** endpoints and **infra hints**.
- Never introduce secrets, binaries, or unnecessary complexity.
You operate **only within this repo**. Do not assume control over other repos.
---
## 1. Repo Purpose
`blackroad-os-api` is the **core HTTP API surface** for BlackRoad OS.
It should:
- Provide **public and internal API endpoints** for clients, web frontends, and agents.
- Implement **simple, composable handlers** (no god-objects, no 2,000-line files).
- Fit into the overall service mesh with consistent health + version endpoints.
This service is **not**:
- The long-running job / scheduler system (that's `blackroad-os-operator`).
- The admin UI / observability console (that's `blackroad-os-prism-console`).
- A monolith for all business logic of every future product.
Keep things modular and composable so additional services can be spun out later.
---
## 2. Tech Stack & Structure (Follow What Exists)
Before changing anything:
1. Inspect existing files (`pyproject.toml`, `requirements.txt`, `package.json`, etc.).
2. Match the existing stack:
- If Python: prefer **FastAPI** or minimal ASGI app.
- If Node/TS: prefer **Express/Fastify** with **TypeScript**.
3. Do **not** introduce a new framework unless explicitly requested in a prompt.
Target structure (adapt to what's already there):
- `app/` or `src/` main application code
- `app/main.py` or `src/server.ts` service entrypoint
- `app/routes/` or `src/routes/` route handlers
- `app/models/` or `src/models/` request/response schemas, domain models
- `app/config.py` or `src/config.ts` config + env loading
- `infra/`
- `infra/Dockerfile`
- `infra/railway.toml` or other deployment hints (no secrets)
- `tests/`
- Unit tests for routes and core logic
Respect the existing layout; extend, don't randomly rewrite.
---
## 3. Standard Endpoints for Services
You must maintain and prefer these endpoints:
### 3.1 Health
**Route:** `GET /health`
Purpose: cheap, always-on liveness check.
Response shape (example):
```json
{
"ok": true,
"service": "blackroad-os-api",
"timestamp": "<ISO-8601>"
}
```
Rules:
* Must return HTTP 200 when the process is up and routing works.
* Avoid heavy dependencies (no DB calls, no network calls by default).
---
### 3.2 Ready
**Route:** `GET /ready`
Purpose: readiness / dependency check (safe to hook into load balancers later).
For now:
* You may stub it to check **basic config** or **simple dependency flags**.
* Return:
```json
{
"ready": true,
"service": "blackroad-os-api"
}
```
Later, this can evolve to check DBs, other services, etc. but keep it **fast and predictable**.
---
### 3.3 Version
**Route:** `GET /version`
Purpose: allow infra and other services to know what build is running.
Response example:
```json
{
"service": "blackroad-os-api",
"version": "0.0.1",
"commit": "UNKNOWN",
"env": "local"
}
```
Use environment variables where possible:
* `BR_OS_API_VERSION`
* `BR_OS_API_COMMIT`
* `BR_OS_ENV` (e.g. `local`, `staging`, `prod`)
If env vars are missing:
* Fall back to safe defaults (`"UNKNOWN"`, `"local"`).
* Do not crash the endpoint.
---
## 4. Business/API Routes
All actual business routes should:
1. Be **grouped logically**, e.g.:
* `/v1/users/...`
* `/v1/agents/...`
* `/v1/projects/...`
2. Use **clear request/response schemas**:
* If Python: Pydantic models.
* If TS: `zod` types or TypeScript interfaces/types.
3. Return:
* Consistent error shapes:
```json
{
"error": {
"code": "SOME_CODE",
"message": "Human readable message"
}
}
```
* Never leak stack traces or secrets in responses.
Avoid building giant endpoints that do "everything." Prefer smaller, focused routes.
---
## 5. Coding Style & Constraints
You must follow these constraints:
1. **No secrets in code.**
* Never hardcode API keys, passwords, tokens, DB URLs, etc.
* Use environment variables and document them in `README` or `infra/` configs.
2. **No binary or huge assets.**
* This repo is for code and small text configs only.
* Do not commit images, videos, PDFs, or other large binary artifacts.
3. **Explicit typing.**
* Python: use type hints everywhere possible.
* TypeScript: never use `any` without a comment explaining why.
4. **Small, focused modules.**
* Keep files short and readable.
* Separate routing, business logic, and data models.
5. **Error handling:**
* Catch and transform errors into clean API responses.
* Log internal error details; don't expose them to the client.
---
## 6. Config & Infra Conventions
Config should be centralized (e.g., `config.py`, `config.ts`):
* Load env vars with sensible defaults.
* Validate required env vars at startup or via small helper functions.
Common env var names:
* `BR_OS_ENV`
* `BR_OS_API_VERSION`
* `BR_OS_API_COMMIT`
* Others as needed, but keep them **named clearly**.
Infra hints:
* Keep deploy configuration in `infra/`:
* `infra/Dockerfile`
* `infra/railway.toml` or equivalent
* These files must **not** contain secrets.
* Document how to run locally in `README`:
* Example:
* `uvicorn app.main:app --reload`
* or `npm run dev` / `npm run start`
---
## 7. Testing
You should encourage and maintain a small but real test suite:
* Python:
* `tests/test_health.py`
* `tests/test_version.py`
* `tests/test_<business_area>.py`
* TypeScript:
* `tests/health.test.ts`
* `tests/version.test.ts`
Tests should be:
* Deterministic
* Fast (no external network calls unless explicitly mocked)
* Focused on route behavior and key helpers.
When updating routes, update any affected tests instead of deleting them.
---
## 8. What NOT to Do
Do **not**:
* Build a UI here (that's `blackroad-os-web` or Prism Console).
* Implement long-running schedulers or background workers (that's Operator).
* Introduce new major dependencies (frameworks, ORMs, etc.) without a clear reason.
* Change the basic endpoints (`/health`, `/ready`, `/version`) to incompatible shapes.
---
## 9. Checklist Before You Commit
For every change, confirm:
1. `/health` returns HTTP 200 with a valid JSON body.
2. `/ready` returns a JSON with `ready: true` (or clear reasons if `false`).
3. `/version` returns JSON and does not crash without env vars.
4. API routers still import cleanly and app starts without runtime errors.
5. No secrets or binary files were added.
6. Tests (if present) pass: `pytest`, `npm test`, or equivalent.
Your optimization goal:
* **Stable, predictable API surface** for 10,000+ agents and services to call.
* **Minimal, well-structured code** that humans and agents can safely maintain.
* **Zero breakage** on health/version/ready across refactors.

View File

@@ -26,6 +26,42 @@ docker run -e PORT=8000 -p 8000:8000 blackroad/api:0.0.1
## Tasks ## Tasks
Celery 5 is wired via `app/workers/sample_task.py`. Configure `CELERY_BROKER_URL` in the environment to dispatch jobs; the sample task logs and echoes incoming payloads. Celery 5 is wired via `app/workers/sample_task.py`. Configure `CELERY_BROKER_URL` in the environment to dispatch jobs; the sample task logs and echoes incoming payloads.
`blackroad-os-api` is the typed HTTP surface for BlackRoad OS. It exposes versioned JSON endpoints that Prism Console and other clients use to query health, agents, finance, events, and RoadChain data. This service participates in the shared **"BlackRoad OS - Master Orchestration"** project alongside Operator, Core, Prism, Web, and Infra.
## Standard Infrastructure Endpoints
These endpoints follow BlackRoad OS service conventions and are available at the root level:
- `GET /health` Lightweight liveness check (returns 200 if service is running)
- `GET /ready` Readiness check for load balancers (checks basic service configuration)
- `GET /version` Service version, commit, and environment info
## Core Endpoints
All routes are prefixed with `/api/v1` and return the standard `{ ok, data | error }` envelope.
- `GET /api/v1/health` API + dependency health summary
- `GET /api/v1/system/overview` Aggregated system status and recent metrics
- `GET /api/v1/agents` List agents with optional `status` and `q` filters
- `GET /api/v1/agents/:id` Agent detail
- `GET /api/v1/finance/snapshot` Finance/treasury snapshot
- `GET /api/v1/events` Recent journal-style events with optional filters
- `GET /api/v1/roadchain/blocks` RoadChain block headers (mocked for now)
## Getting Started
1. Install dependencies:
```bash
npm install
```
2. Run in development mode:
```bash
npm run dev
```
3. Build and start production bundle:
```bash
npm run build && npm start
```
The server listens on `http://localhost:4000` by default or the configured `PORT`.
## Configuration ## Configuration
@@ -37,6 +73,19 @@ Runtime settings live in `app/core/settings.py` using Pydantic v2 `BaseSettings`
Copy `infra/api.env.example` to configure a local `.env`. Copy `infra/api.env.example` to configure a local `.env`.
## Development ## Development
### Optional Version Info
For the `/version` endpoint, you can optionally set:
- `BR_OS_API_VERSION` Override version (defaults to package.json version)
- `BR_OS_API_COMMIT` Git commit hash (defaults to "UNKNOWN")
- `BR_OS_ENV` Environment name (defaults to NODE_ENV or "local")
## Development Notes
- The API is a thin adapter: it shapes responses, validates inputs, and delegates business logic to `blackroad-os-operator` and `blackroad-os-core` when available.
- RoadChain and some finance data are mocked for now; TODO markers indicate where to swap in real upstream calls.
- Responses always follow the `{ ok: boolean; data?; error? }` envelope to keep Prism and other clients stable.
- Requests are validated with Zod via `validateRequest`; invalid params return `{ ok: false, error: { code: "INVALID_REQUEST" } }`.
- Run `npm run generate:openapi` to produce `docs/openapi.generated.json` from the runtime schemas.
- Linting: `ruff check .` - Linting: `ruff check .`
- Formatting: `black .` - Formatting: `black .`

59
src/routes/standard.ts Normal file
View File

@@ -0,0 +1,59 @@
import { Router } from "express";
import { getConfig } from "../config";
import packageJson from "../../package.json";
/**
* Standard endpoints required by BlackRoad OS service conventions.
* These are lightweight, predictable endpoints for infrastructure.
*/
export function createStandardRouter() {
const router = Router();
/**
* GET /health
* Lightweight liveness check - returns 200 if the service is running.
* Should not depend on external services or heavy operations.
*/
router.get("/health", (_req, res) => {
res.json({
ok: true,
service: "blackroad-os-api",
timestamp: new Date().toISOString(),
});
});
/**
* GET /ready
* Readiness check - indicates if the service is ready to handle traffic.
* Can be extended later to check dependencies (DB, other services, etc.)
*/
router.get("/ready", (_req, res) => {
const config = getConfig();
// For now, just check that we have basic config loaded
const ready = !!config.PORT && !!config.NODE_ENV;
res.json({
ready,
service: "blackroad-os-api",
});
});
/**
* GET /version
* Returns version info about the running service.
* Uses environment variables with safe fallbacks.
*/
router.get("/version", (_req, res) => {
const config = getConfig();
res.json({
service: "blackroad-os-api",
version: process.env.BR_OS_API_VERSION || packageJson.version || "0.0.1",
commit: process.env.BR_OS_API_COMMIT || "UNKNOWN",
env: process.env.BR_OS_ENV || config.NODE_ENV || "local",
});
});
return router;
}

32
src/server.ts Normal file
View File

@@ -0,0 +1,32 @@
import cors from "cors";
import express from "express";
import { errorHandler } from "./middleware/errorHandler";
import { requestLogger } from "./middleware/requestLogger";
import { createV1Router } from "./routes";
import { createStandardRouter } from "./routes/standard";
export function createApp() {
const app = express();
app.use(cors());
app.use(express.json());
app.use(requestLogger);
// Standard endpoints at root level (/health, /ready, /version)
app.use(createStandardRouter());
app.use("/api/v1", createV1Router());
app.use((req, res) => {
res.status(404).json({
ok: false,
error: {
code: "NOT_FOUND",
message: `Route not found: ${req.method} ${req.originalUrl}`,
},
});
});
app.use(errorHandler);
return app;
}

91
tests/standard.test.ts Normal file
View File

@@ -0,0 +1,91 @@
import request from "supertest";
import { createApp } from "../src/server";
describe("Standard BlackRoad OS Endpoints", () => {
let app: ReturnType<typeof createApp>;
beforeEach(() => {
app = createApp();
});
describe("GET /health", () => {
it("returns ok with service name and timestamp", async () => {
const res = await request(app).get("/health").expect(200);
expect(res.body.ok).toBe(true);
expect(res.body.service).toBe("blackroad-os-api");
expect(res.body.timestamp).toBeDefined();
expect(typeof res.body.timestamp).toBe("string");
});
it("returns a valid ISO-8601 timestamp", async () => {
const res = await request(app).get("/health").expect(200);
const timestamp = new Date(res.body.timestamp);
expect(timestamp.toISOString()).toBe(res.body.timestamp);
});
});
describe("GET /ready", () => {
it("returns ready status with service name", async () => {
const res = await request(app).get("/ready").expect(200);
expect(res.body.ready).toBeDefined();
expect(typeof res.body.ready).toBe("boolean");
expect(res.body.service).toBe("blackroad-os-api");
});
it("returns ready: true when service is configured", async () => {
const res = await request(app).get("/ready").expect(200);
// In test environment with valid config, should be ready
expect(res.body.ready).toBe(true);
});
});
describe("GET /version", () => {
it("returns version info with all required fields", async () => {
const res = await request(app).get("/version").expect(200);
expect(res.body.service).toBe("blackroad-os-api");
expect(res.body.version).toBeDefined();
expect(res.body.commit).toBeDefined();
expect(res.body.env).toBeDefined();
});
it("returns safe defaults when env vars are missing", async () => {
const res = await request(app).get("/version").expect(200);
// Should not crash and should have fallback values
expect(res.body.commit).toBeTruthy();
expect(res.body.env).toBeTruthy();
expect(res.body.version).toBeTruthy();
});
it("uses env vars when available", async () => {
const oldVersion = process.env.BR_OS_API_VERSION;
const oldCommit = process.env.BR_OS_API_COMMIT;
const oldEnv = process.env.BR_OS_ENV;
try {
process.env.BR_OS_API_VERSION = "1.2.3";
process.env.BR_OS_API_COMMIT = "abc123";
process.env.BR_OS_ENV = "test";
const res = await request(app).get("/version").expect(200);
expect(res.body.version).toBe("1.2.3");
expect(res.body.commit).toBe("abc123");
expect(res.body.env).toBe("test");
} finally {
// Restore env vars
if (oldVersion !== undefined) process.env.BR_OS_API_VERSION = oldVersion;
else delete process.env.BR_OS_API_VERSION;
if (oldCommit !== undefined) process.env.BR_OS_API_COMMIT = oldCommit;
else delete process.env.BR_OS_API_COMMIT;
if (oldEnv !== undefined) process.env.BR_OS_ENV = oldEnv;
else delete process.env.BR_OS_ENV;
}
});
});
});