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
10 changes: 8 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ REDIS_PORT=6379
NATS_PORT=4222
NATS_MON_PORT=8222

# Bot fleet per-instance concurrency. Total bots = replicas × this.
# A laptop comfortably runs 4 replicas × 50 = 200 concurrent bots.
# Bot fleet per-instance concurrency. Total bots = replicas x this.
# A laptop comfortably runs 4 replicas x 50 = 200 concurrent bots.
BOTS_PER_INSTANCE=50

# AI Analyzer: Gemini API key for multi-agent code analysis.
# Get one at: https://aistudio.google.com/app/apikey
# Leave empty to disable AI analysis features.
GEMINI_API_KEY=
# GEMINI_MODEL=gemini-2.5-flash
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
**/build/
**/bin/
**/*.exe
examples/sample-engine-go/sample-engine

# Go
**/*.test
Expand Down
10 changes: 9 additions & 1 deletion Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,15 @@
:80 {
encode gzip zstd

# API + WebSocket → gateway
# AI analysis endpoints route to ai-analyzer service
handle /api/analyze {
reverse_proxy ai-analyzer:7080
}
handle /api/report {
reverse_proxy ai-analyzer:7080
}

# All other API + WebSocket routes go to gateway
handle /api/* {
reverse_proxy gateway:7070
}
Expand Down
433 changes: 433 additions & 0 deletions DESIGN.md

Large diffs are not rendered by default.

65 changes: 54 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ That brings up:
|---|---|---|
| Caddy (frontend + reverse proxy) | 8080 | Open this in a browser |
| Gateway (HTTP/WS API) | internal | `/api/*`, `/ws/*` |
| AI Analyzer | internal | Multi-agent code analysis via Gemini |
| Bot fleet | internal | Load generator (scale with `--scale botfleet=N`) |
| Telemetry ingester | internal | NATS TimescaleDB |
| Telemetry ingester | internal | NATS -> TimescaleDB |
| TimescaleDB | 5432 | Time-series DB |
| Redis | 6379 | Hot state |
| NATS | 4222 | Message bus |
Expand Down Expand Up @@ -62,7 +63,13 @@ wscat -c ws://localhost:8080/ws/runs/run_yyy
# 6. After 30s, see the leaderboard
curl http://localhost:8080/api/leaderboard | jq .

# 7. Scale the bot fleet horizontally
# 7. AI code analysis (requires GEMINI_API_KEY in .env)
curl -H "Content-Type: application/json" -X POST \
-d '{"sourceCode":"package main\nfunc submit(o Order) {}"}' \
http://localhost:8080/api/analyze | jq .
# -> {"riskScore":45,"findings":[...],"recommendations":[...]}

# 8. Scale the bot fleet horizontally
docker compose up -d --scale botfleet=4
```

Expand Down Expand Up @@ -158,6 +165,7 @@ iicpc-platform/
├── LIMITATIONS.md — what's not built and why
├── docker-compose.yml — one-command local stack
├── Caddyfile — edge / reverse proxy
├── .env.example — env config (copy to .env)
├── sql/init.sql — TimescaleDB schema + hypertable + cagg
├── frontend/ — static UI (HTML/CSS/JS, design bundle)
│ ├── index.html — public landing
Expand All @@ -166,6 +174,7 @@ iicpc-platform/
│ ├── submit.html
│ ├── run.html
│ ├── correctness.html
│ ├── analyze.html — AI code analysis page (NEW)
│ ├── leaderboard.html
│ ├── judge.html
│ ├── architecture.html
Expand All @@ -180,25 +189,29 @@ iicpc-platform/
│ │ ├── cache/ — Redis pubsub + ZSET
│ │ ├── bus/ — NATS / JetStream
│ │ └── sandbox/ — docker build + run with strict flags
│ ├── ai-analyzer/ — Go: Multi-agent code review via Gemini (NEW)
│ │ ├── cmd/main.go — HTTP API for /api/analyze, /api/report
│ │ └── internal/
│ │ ├── agents/ — security, performance, correctness agents + synthesizer
│ │ ├── gemini/ — raw HTTP Gemini API client (no SDK)
│ │ └── report/ — post-run performance report generator
│ ├── botfleet/ — Go: goroutine-per-bot, fasthttp client
│ │ ├── cmd/main.go
│ │ └── internal/bot/ — bot loop + xoshiro256** RNG
│ └── telemetry/ — Go: NATS → batched COPY → TimescaleDB
│ └── telemetry/ — Go: NATS → batched COPY → TimescaleDB + Redis ZADD
│ └── cmd/main.go
├── tests/ — standalone unit tests (27 tests)
│ ├── sandbox_test.go — archive extraction, path traversal, Dockerfile validation
│ ├── scoring_test.go — composite score math, edge cases
│ └── agent_test.go — risk scoring, recommendation dedup, strengths
├── examples/
│ └── sample-engine-go/ — reference matching engine (the "submission")
│ ├── Dockerfile
│ └── main.go
├── .github/workflows/ci.yml — CI pipeline: build + vet + test + docker
├── terraform/ — single-EC2 AWS deploy
│ ├── main.tf
│ ├── variables.tf
│ ├── outputs.tf
│ └── cloud-init.yaml
├── k8s/ — production Kubernetes manifests
│ ├── namespace.yaml — namespace + ResourceQuota + NetworkPolicy
│ ├── datastores.yaml — TimescaleDB + Redis + NATS StatefulSets
│ ├── services.yaml — gateway + botfleet + telemetry Deployments + HPAs
│ └── ingress.yaml — Caddy + Ingress
├── deploy/digitalocean/ — doctl + cloud-init deploy
├── scripts/
│ └── demo.sh — end-to-end demo script
└── docs/ — additional diagrams (if any)
Expand Down Expand Up @@ -228,4 +241,34 @@ go test ./...
go run ./cmd
```

Run unit tests (no external deps needed):

```bash
cd tests
go test -v -count=1 -race ./...
# 27 tests: sandbox extraction, scoring math, agent risk scoring
```

Run the full stack via `docker compose up --build` and iterate. Hot-reload isn't wired (`reflex` or `air` would do it); for now `docker compose up --build gateway` rebuilds just that service.

### AI Analysis Setup

```bash
# 1. Get a Gemini API key from https://aistudio.google.com/app/apikey
# 2. Add it to your .env file
cp .env.example .env
echo "GEMINI_API_KEY=your-key-here" >> .env

# 3. Rebuild and start
docker compose up --build ai-analyzer
```

---

## Test Coverage

| Test File | Tests | What it verifies |
|---|---|---|
| `sandbox_test.go` | 6 | tar.gz/zip extraction, Dockerfile validation, path traversal protection |
| `scoring_test.go` | 13 | Composite scoring formula, edge cases (zero, negative, overflow) |
| `agent_test.go` | 8 | Risk score computation, recommendation dedup, strength detection |
18 changes: 18 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,23 @@ services:
environment:
- DATABASE_URL=postgres://iicpc:iicpc@timescale:5432/iicpc?sslmode=disable
- NATS_URL=nats://nats:4222
- REDIS_URL=redis://redis:6379
depends_on:
timescale: { condition: service_healthy }
nats: { condition: service_started }
redis: { condition: service_started }
networks: [iicpc-net]

ai-analyzer:
build:
context: ./services/ai-analyzer
dockerfile: Dockerfile
restart: unless-stopped
environment:
- GEMINI_API_KEY=${GEMINI_API_KEY:-}
- GEMINI_MODEL=${GEMINI_MODEL:-gemini-2.5-flash}
- PORT=7080
expose: ["7080"]
networks: [iicpc-net]

timescale:
Expand Down Expand Up @@ -149,4 +163,8 @@ volumes:

networks:
iicpc-net:
# Pin an explicit name (otherwise Compose prefixes it as
# "<project>_iicpc-net"). The gateway spawns contestant sandboxes with
# `docker run --network iicpc-net`, so the real network name must match.
name: iicpc-net
driver: bridge
14 changes: 9 additions & 5 deletions examples/sample-engine-go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,15 +129,15 @@ func (b *book) removeOrder(side string, px int64, id int64) bool {
type engine struct {
bk book
idx map[int64]struct{ Side string; Px int64; ClientID int64 } // id → location
seenIDs map[int64]struct{}
seenIDs map[[2]int64]struct{} // (clientId,id) → seen; composite so different bots may reuse the same order-id space
fillIDSeq atomic.Int64
stpPolicy string // none|cancel-taker|cancel-maker|cancel-both
}

func newEngine() *engine {
return &engine{
idx: map[int64]struct{ Side string; Px int64; ClientID int64 }{},
seenIDs: map[int64]struct{}{},
seenIDs: map[[2]int64]struct{}{},
stpPolicy: "none",
}
}
Expand Down Expand Up @@ -184,7 +184,10 @@ func (e *engine) submit(req orderReq) submitResp {
r := submitResp{Acks: []ack{}, Fills: []fill{}}

// Validate ----------------------------------------------------
pxX100 := int64(req.Price * 100)
// Price arrives already in integer ticks (price * 100); the bot fleet and
// the telemetry `priceX100` field use this same convention, so we do NOT
// re-scale here (previously `req.Price * 100`, which double-scaled by 100x).
pxX100 := int64(req.Price)
qty := int64(req.Qty)

if req.Type == "cancel" || req.Type == "modify" {
Expand All @@ -202,12 +205,13 @@ func (e *engine) submit(req orderReq) submitResp {
r.Acks = append(r.Acks, ack{ID: req.ID, Status: "rejected", Reason: "bad-price"})
return r
}
if _, dup := e.seenIDs[req.ID]; dup && req.ID != 0 {
dedupKey := [2]int64{req.ClientID, req.ID}
if _, dup := e.seenIDs[dedupKey]; dup && req.ID != 0 {
r.Acks = append(r.Acks, ack{ID: req.ID, Status: "rejected", Reason: "duplicate-id"})
return r
}
if req.ID != 0 {
e.seenIDs[req.ID] = struct{}{}
e.seenIDs[dedupKey] = struct{}{}
}
}

Expand Down
Loading