Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,8 @@ env/

### Node.js
node_modules/
web/.next/
web/out/
web/.turbo/
*.tsbuildinfo
.pnpm-store/
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ K8S_NAMESPACE ?= agentenv-system
K8S_RUNTIME_IMAGE ?= agentenv-runtime:latest
K8S_GATEWAY_IMAGE ?= agentenv-gateway:latest
K8S_SCHEDULER_IMAGE ?= agentenv-scheduler:latest
K8S_WEB_IMAGE ?= agentenv-web:latest
K3S_CTR ?= sudo k3s ctr

# aenv home path.
Expand Down Expand Up @@ -237,19 +238,23 @@ k8s-build:
$(DOCKER) build $(if $(APT_MIRROR_BASE),--build-arg APT_MIRROR_BASE="$(APT_MIRROR_BASE)",) -f deploy/docker/Dockerfile.agentenv -t $(K8S_RUNTIME_IMAGE) .
$(DOCKER) build -f deploy/docker/Dockerfile.gateway -t $(K8S_GATEWAY_IMAGE) .
$(DOCKER) build -f deploy/docker/Dockerfile.scheduler -t $(K8S_SCHEDULER_IMAGE) .
$(DOCKER) build -f deploy/docker/Dockerfile.web -t $(K8S_WEB_IMAGE) web

k8s-redeploy:
$(KUBECTL) rollout restart deploy/agentenv-gateway -n $(K8S_NAMESPACE)
$(KUBECTL) rollout restart deploy/agentenv-scheduler -n $(K8S_NAMESPACE)
$(KUBECTL) rollout restart deploy/agentenv-web -n $(K8S_NAMESPACE)
$(KUBECTL) rollout restart ds/agentenv-node -n $(K8S_NAMESPACE)
$(KUBECTL) rollout status deploy/agentenv-gateway -n $(K8S_NAMESPACE)
$(KUBECTL) rollout status deploy/agentenv-scheduler -n $(K8S_NAMESPACE)
$(KUBECTL) rollout status deploy/agentenv-web -n $(K8S_NAMESPACE)
$(KUBECTL) rollout status ds/agentenv-node -n $(K8S_NAMESPACE)

k8s-load-dev:
$(DOCKER) save $(K8S_RUNTIME_IMAGE) | $(K3S_CTR) images import -
$(DOCKER) save $(K8S_GATEWAY_IMAGE) | $(K3S_CTR) images import -
$(DOCKER) save $(K8S_SCHEDULER_IMAGE) | $(K3S_CTR) images import -
$(DOCKER) save $(K8S_WEB_IMAGE) | $(K3S_CTR) images import -

k8s-refresh-dev: k8s-build k8s-load-dev k8s-redeploy

Expand Down
16 changes: 16 additions & 0 deletions deploy/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,22 @@ services:
- "8080:8080"
restart: unless-stopped

web:
build:
context: ../web
dockerfile: ../deploy/docker/Dockerfile.web
image: agentenv-web:latest
container_name: agentenv-web
depends_on:
- gateway
ports:
- "3000:3000"
environment:
NODE_ENV: production
PORT: "3000"
AENV_DEFAULT_GATEWAY_URL: ${AENV_DEFAULT_GATEWAY_URL:-http://127.0.0.1:8080}
restart: unless-stopped

agentenv-a:
<<: *agentenv-base
build:
Expand Down
32 changes: 32 additions & 0 deletions deploy/docker/Dockerfile.web
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
FROM node:22-bookworm-slim AS deps
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@11.0.8 --activate
COPY package.json pnpm-lock.yaml ./
COPY pnpm-workspace.yaml* ./
RUN pnpm install --frozen-lockfile

FROM node:22-bookworm-slim AS builder
WORKDIR /app
RUN corepack enable && corepack prepare pnpm@11.0.8 --activate
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN pnpm build

FROM node:22-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
ENV HOSTNAME=0.0.0.0

RUN groupadd --system --gid 1001 nodejs \
&& useradd --system --uid 1001 --gid nodejs nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
5 changes: 5 additions & 0 deletions deploy/k8s/base/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ resources:
- scheduler-deployment.yaml
- gateway-service.yaml
- gateway-deployment.yaml
- web-service.yaml
- web-deployment.yaml
- agentenv-headless-service.yaml
- agentenv-ublk-daemon-metrics-service.yaml
- agentenv-daemonset.yaml
Expand Down Expand Up @@ -40,6 +42,9 @@ images:
- name: agentenv-runtime
newName: agentenv-runtime
newTag: latest
- name: agentenv-web
newName: agentenv-web
newTag: latest

generatorOptions:
disableNameSuffixHash: true
50 changes: 50 additions & 0 deletions deploy/k8s/base/web-deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: agentenv-web
labels:
app.kubernetes.io/name: agentenv-web
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: agentenv-web
template:
metadata:
labels:
app.kubernetes.io/name: agentenv-web
spec:
nodeSelector:
kubernetes.io/os: linux
containers:
- name: web
image: agentenv-web:latest
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 3000
env:
- name: NODE_ENV
value: production
- name: PORT
value: "3000"
- name: AENV_DEFAULT_GATEWAY_URL
value: http://agentenv-gateway:8080
readinessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet:
path: /
port: http
initialDelaySeconds: 15
periodSeconds: 20
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 512Mi
13 changes: 13 additions & 0 deletions deploy/k8s/base/web-service.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: v1
kind: Service
metadata:
name: agentenv-web
labels:
app.kubernetes.io/name: agentenv-web
spec:
selector:
app.kubernetes.io/name: agentenv-web
ports:
- name: http
port: 3000
targetPort: http
1 change: 1 addition & 0 deletions docs/src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
- [Quick Start](./getting-started/quickstart.md)
- [On-Demand Loading](./getting-started/on-demand-loading.md)
- [aenv CLI Reference](./getting-started/aenv-cli.md)
- [Web UI](./getting-started/web-ui.md)

# Deployment

Expand Down
1 change: 1 addition & 0 deletions docs/src/deployment/docker-compose.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Run a full multi-node stack on a single host using Docker Compose. This simulate
|---------|------|-------------|
| Gateway | `:8080` | HTTP/WebSocket reverse proxy |
| Scheduler | `:9090` | gRPC node selection and sandbox binding |
| Web UI | `:3000` | Control-plane console (Next.js) |
| agentenv-a | `:8001` | AgentENV runtime node A |
| agentenv-b | `:8002` | AgentENV runtime node B |

Expand Down
11 changes: 10 additions & 1 deletion docs/src/deployment/kubernetes.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Deploy AgentENV across a Kubernetes cluster with a gateway, scheduler, and runti
|----------|------|-------------|
| `agentenv-gateway` | Deployment + ClusterIP Service | HTTP reverse proxy for client traffic |
| `agentenv-scheduler` | Deployment (single replica) + ClusterIP Service | gRPC node selection and sandbox binding |
| `agentenv-web` | Deployment + ClusterIP Service | Control-plane Web UI (Next.js) |
| `agentenv-node` | DaemonSet (privileged) | One runtime Pod per Kubernetes node |
| `agentenv-nodes` | Headless Service | Used by the scheduler for EndpointSlice discovery |

Expand Down Expand Up @@ -37,7 +38,7 @@ cd AgentENV
make k8s-build
```

This builds three images: `agentenv-runtime:latest`, `agentenv-gateway:latest`, and `agentenv-scheduler:latest`.
This builds four images: `agentenv-runtime:latest`, `agentenv-gateway:latest`, `agentenv-scheduler:latest`, and `agentenv-web:latest`.

## Deploy

Expand Down Expand Up @@ -66,6 +67,14 @@ wildcard DNS for `*.sandbox.example.com`.

The default overlay is `deploy/k8s/overlays/default`, targeting the `agentenv-system` namespace. The gateway is exposed as ClusterIP by default. Add your own Ingress or LoadBalancer for external access.

Access the Web UI with:

```bash
kubectl -n agentenv-system port-forward svc/agentenv-web 3000:3000
```

Then open [http://127.0.0.1:3000](http://127.0.0.1:3000). See [Web UI](../getting-started/web-ui.md).

The make targets build a temporary Kustomize context so runtime Pods mount the repository's `config/default.toml` rather than a separate checked-in copy.

The runtime DaemonSet injects scheduler-report wiring for each node Pod:
Expand Down
50 changes: 50 additions & 0 deletions docs/src/getting-started/web-ui.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Web UI (control plane)

The AgentENV Web UI is a Next.js console for operators and developers to manage sandboxes, snapshots, templates, and nodes through the **Gateway HTTP API**.

## Prerequisites

- A running AgentENV Gateway (single-node `:8000` or multi-node Gateway `:8080`)
- An API key header value (`X-API-Key`) accepted by the deployment
- Optional admin token (`X-Admin-Token`) for `/nodes` APIs

## Local development

```bash
cd web
pnpm install
pnpm dev
```

Open [http://localhost:3000](http://localhost:3000). In **Settings**, set:

| Field | Example (Compose Gateway) | Example (single node) |
|---|---|---|
| Gateway URL | `http://127.0.0.1:8080` | `http://127.0.0.1:8000` |
| API key | any non-empty key | same |
| Admin token | optional | optional |

Credentials are stored in httpOnly session cookies. They are cleared on logout and, because the cookies carry no expiry, when the browser session ends.

## Docker Compose

The `web` service is defined in `deploy/docker-compose.yml` and published on host port **3000**.

```bash
docker compose -f deploy/docker-compose.yml up -d --build web
```

Then open [http://127.0.0.1:3000](http://127.0.0.1:3000) and point Settings at `http://127.0.0.1:8080` (Gateway published on the host).

Build context for the image is `web/` using `deploy/docker/Dockerfile.web`.

## Kubernetes

Kustomize base includes `agentenv-web` Deployment + Service (`deploy/k8s/base/`).

```bash
make k8s-apply
kubectl -n agentenv-system port-forward svc/agentenv-web 3000:3000
```

Default in-cluster Gateway URL env: `http://agentenv-gateway:8080`. When using the UI from a browser on your laptop via port-forward, set Settings to the Gateway URL **reachable from the Next.js server** (in-cluster service name if server-side fetches run in-pod) or use host-accessible URLs consistently.
13 changes: 13 additions & 0 deletions web/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
node_modules
.next
.git
.gitignore
*.md
.env*
.env*.local
eslint.config.*
.next/
out/
coverage/
.turbo
.DS_Store
41 changes: 41 additions & 0 deletions web/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
31 changes: 31 additions & 0 deletions web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# AgentENV Web UI (`web/`)

Control-plane console for [Issue #6](https://github.com/kvcache-ai/AgentENV/issues/6).

## Stack

- Next.js App Router + TypeScript
- Tailwind CSS v4 + shadcn/ui
- pnpm

## Develop

```bash
cd web
pnpm install
pnpm dev
```

Open http://localhost:3000 — configure Gateway (default Compose `:8080`) under **Settings**.

## Conventions

- Talk to **Gateway HTTP only** (never Scheduler gRPC).
- Credentials live in **httpOnly cookies** (`src/lib/session.ts`); never log full secrets.
- Upstream calls: `src/lib/api/client.ts` (`gatewayFetch`).
- Feature routes live under `src/app/(console)/`.
- Use existing shadcn components in `src/components/ui/`.

## Non-goals (v1)

No browser terminal, filesystem browser, or in-sandbox process execution.
25 changes: 25 additions & 0 deletions web/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"rtl": false,
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"menuColor": "default",
"menuAccent": "subtle",
"registries": {}
}
Loading
Loading