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
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,17 @@ MULTICODEX_RETENTION_MAX_AGE=720h
MULTICODEX_RETENTION_DRY_RUN=true
MULTICODEX_QUEUE_ENABLED=true
MULTICODEX_QUEUE_DISPATCH_INTERVAL=5s
MULTICODEX_ORG_ACTIVE_RUN_LIMIT=100
MULTICODEX_PROJECT_ACTIVE_RUN_LIMIT=20
MULTICODEX_ORG_QUEUED_RUN_LIMIT=500
MULTICODEX_PROJECT_QUEUED_RUN_LIMIT=100
MULTICODEX_API_MUTATION_RATE_LIMIT=120
MULTICODEX_API_MUTATION_RATE_WINDOW=1m
MULTICODEX_WORKER_START_RATE_LIMIT=60
MULTICODEX_WORKER_START_RATE_WINDOW=1m
MULTICODEX_USAGE_BUDGET_ENABLED=false
MULTICODEX_USAGE_BUDGET_SECONDS_PER_DAY=0
MULTICODEX_USAGE_BUDGET_WARN_RATIO=0.8
MULTICODEX_TELEMETRY_PUSH_URL=
MULTICODEX_TELEMETRY_PUSH_INTERVAL=1m
MULTICODEX_AUDIT_EXPORT_PATH=
Expand Down
100 changes: 100 additions & 0 deletions .env.production.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Production environment template
# Copy to .env.production and inject secrets from a secret manager.
# Do not commit filled values.

POSTGRES_DB=multi_codex
POSTGRES_USER=multi_codex
POSTGRES_PASSWORD=

MULTICODEX_ENV=production
MULTICODEX_PRODUCTION=true
MULTICODEX_CORS_ALLOWED_ORIGINS=https://multi-codex.example.com

MULTICODEX_API_LISTEN=:8080
MULTICODEX_MCP_LISTEN=:8090
MULTICODEX_AGENTD_LISTEN=:7070
MULTICODEX_AGENTD_URL=http://worker-agentd:7070
MULTICODEX_AGENTD_TOKEN=

MULTICODEX_DATABASE_URL=postgres://multi_codex:CHANGE_ME@postgres:5432/multi_codex?sslmode=require
MULTICODEX_ARTIFACT_ROOT=/var/lib/multi-codex/artifacts
MULTICODEX_RUN_ROOT=/var/lib/multi-codex/runs
MULTICODEX_WORKTREE_ROOT=/var/lib/multi-codex/worktrees
MULTICODEX_REPO_CACHE_ROOT=/var/lib/multi-codex/repos

MULTICODEX_API_IMAGE=ghcr.io/acme/multi-codex/api:v0.1.0
MULTICODEX_WEB_IMAGE=ghcr.io/acme/multi-codex/web:v0.1.0
MULTICODEX_WORKER_IMAGE=ghcr.io/acme/multi-codex/codex-worker:v0.1.0

MULTICODEX_EXECUTOR_MODE=docker
MULTICODEX_WORKER_DOCKER_SOCKET_ENABLED=true
MULTICODEX_WORKER_DOCKER_SOCKET_BOUNDARY=isolated-worker-host
MULTICODEX_WORKER_DEFAULT_TIMEOUT=1h
MULTICODEX_WORKER_CPUS=1
MULTICODEX_WORKER_MEMORY=2g
MULTICODEX_WORKER_PIDS_LIMIT=256
MULTICODEX_WORKER_READ_ONLY_ROOTFS=true
MULTICODEX_WORKER_TMPFS_SIZE=256m
MULTICODEX_WORKER_NO_NEW_PRIVILEGES=true
MULTICODEX_WORKER_CAP_DROP=ALL
MULTICODEX_WORKER_COMMAND_DENYLIST=rm -rf /,curl | sh,wget | sh,chmod 777
MULTICODEX_WORKER_COMMAND_ALLOWLIST=
MULTICODEX_WORKER_SECRET_ENV_ALLOWLIST=OPENAI_API_KEY,CODEX_AUTH_TOKEN
MULTICODEX_WORKER_SECRET_PROVIDER=file
MULTICODEX_WORKER_SECRET_FILE_PATH=/run/secrets/multi-codex-worker.json

MULTICODEX_AUTH_MODE=oidc
MULTICODEX_AUTH_SESSION_TTL=12h
MULTICODEX_AUTH_COOKIE_SECURE=true
MULTICODEX_AUTH_LOGIN_STATE_TTL=10m
MULTICODEX_OIDC_ISSUER=https://issuer.example
MULTICODEX_OIDC_AUDIENCE=multi-codex
MULTICODEX_OIDC_JWKS_URL=https://issuer.example/.well-known/jwks.json
MULTICODEX_OIDC_CLIENT_ID=multi-codex-web
MULTICODEX_OIDC_CLIENT_SECRET=
MULTICODEX_OIDC_CLIENT_AUTH_METHOD=client_secret_post
MULTICODEX_OIDC_REDIRECT_URL=https://multi-codex.example.com/api/v1/auth/callback
MULTICODEX_OIDC_AUTHORIZATION_URL=
MULTICODEX_OIDC_TOKEN_URL=
MULTICODEX_OIDC_POST_LOGIN_REDIRECT_URL=/
MULTICODEX_OIDC_DEFAULT_ROLE=viewer
MULTICODEX_OIDC_DEFAULT_ORG_ID=
MULTICODEX_OIDC_GROUP_ROLE_MAP=engineering=operator;security=auditor
MULTICODEX_OIDC_GROUP_ORG_MAP=

MULTICODEX_RETENTION_ENABLED=true
MULTICODEX_RETENTION_INTERVAL=1h
MULTICODEX_RETENTION_MAX_AGE=720h
MULTICODEX_RETENTION_DRY_RUN=true

MULTICODEX_QUEUE_ENABLED=true
MULTICODEX_QUEUE_DISPATCH_INTERVAL=5s
MULTICODEX_ORG_ACTIVE_RUN_LIMIT=100
MULTICODEX_PROJECT_ACTIVE_RUN_LIMIT=20
MULTICODEX_ORG_QUEUED_RUN_LIMIT=500
MULTICODEX_PROJECT_QUEUED_RUN_LIMIT=100
MULTICODEX_API_MUTATION_RATE_LIMIT=120
MULTICODEX_API_MUTATION_RATE_WINDOW=1m
MULTICODEX_WORKER_START_RATE_LIMIT=60
MULTICODEX_WORKER_START_RATE_WINDOW=1m

MULTICODEX_USAGE_BUDGET_ENABLED=false
MULTICODEX_USAGE_BUDGET_SECONDS_PER_DAY=864000
MULTICODEX_USAGE_BUDGET_WARN_RATIO=0.8

MULTICODEX_TELEMETRY_PUSH_URL=
MULTICODEX_TELEMETRY_PUSH_INTERVAL=1m

MULTICODEX_AUDIT_EXPORT_PATH=
MULTICODEX_AUDIT_SHIP_ENABLED=true
MULTICODEX_AUDIT_SHIP_INTERVAL=24h
MULTICODEX_AUDIT_SEAL_ROOT=/var/lib/multi-codex/audit-seals/scheduled
MULTICODEX_AUDIT_SHIP_TARGET=file:///mnt/worm/multi-codex
MULTICODEX_AUDIT_SHIP_ALLOW_LEGACY_HASH_MISMATCH=false

MULTICODEX_GIT_SYNC_MODE=dry-run
MULTICODEX_GIT_SYNC_LIVE_REVIEWED=false
MULTICODEX_GIT_CREDENTIAL_PROVIDER=file
MULTICODEX_GIT_CREDENTIAL_FILE_PATH=/run/secrets/multi-codex-git.json
MULTICODEX_GITHUB_API_URL=https://api.github.com
MULTICODEX_GITLAB_API_URL=https://gitlab.com/api/v4
9 changes: 8 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ jobs:
run: docker compose -f deployments/docker/compose.dev.yaml run --rm dev go test ./...

- name: Backend build
run: docker compose -f deployments/docker/compose.dev.yaml run --rm dev go build ./cmd/api ./cmd/mcp-gateway ./cmd/worker-agentd ./cmd/mcxctl
run: docker compose -f deployments/docker/compose.dev.yaml run --rm -e GOFLAGS=-buildvcs=false dev go build ./cmd/api ./cmd/mcp-gateway ./cmd/worker-agentd ./cmd/mcxctl

- name: Frontend lint
run: docker compose -f deployments/docker/compose.dev.yaml run --rm dev bash -c 'pnpm --dir apps/web install --frozen-lockfile && pnpm --dir apps/web lint'

- name: Frontend unit tests
run: docker compose -f deployments/docker/compose.dev.yaml run --rm dev bash -c 'pnpm --dir apps/web install --frozen-lockfile && pnpm --dir apps/web test'

- name: Frontend build
run: docker compose -f deployments/docker/compose.dev.yaml run --rm dev bash -c 'pnpm --dir apps/web install --frozen-lockfile && pnpm --dir apps/web build'
Expand All @@ -40,6 +46,7 @@ jobs:
run: |
docker compose -f deployments/docker/compose.dev.yaml config >/dev/null
docker compose -f deployments/docker/compose.yaml config >/dev/null
docker compose -f deployments/docker/compose.yaml -f deployments/docker/compose.ha.yaml config >/dev/null

- name: Migration check
run: |
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.env
.env.*
!.env.example
!.env.production.example

# Go
bin/
Expand All @@ -20,6 +21,9 @@ apps/web/.vite/
apps/web/.pnpm-store/
apps/web/vite.config.d.ts
apps/web/vite.config.js
apps/web/test-results/
apps/web/playwright-report/
apps/web/coverage/

# Local runtime data
.cache/
Expand Down
11 changes: 9 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ COMPOSE ?= docker compose
COMPOSE_DEV := $(COMPOSE) -f deployments/docker/compose.dev.yaml
COMPOSE_PROD := $(COMPOSE) -f deployments/docker/compose.yaml

.PHONY: dev-image worker-image ensure-dev-image dev-shell dev-up dev-down backend-test backend-build frontend-install frontend-build compose-config migrate-dev
.PHONY: dev-image worker-image ensure-dev-image dev-shell dev-up dev-down backend-test backend-build frontend-install frontend-lint frontend-test frontend-build compose-config migrate-dev

dev-image:
docker build -f deployments/docker/Dockerfile.dev -t $(DEV_IMAGE_TAG) .
Expand All @@ -31,17 +31,24 @@ backend-test:
GOCACHE=$${GOCACHE:-$(PWD)/.cache/go-build} go test ./...

backend-build:
GOCACHE=$${GOCACHE:-$(PWD)/.cache/go-build} go build ./cmd/api ./cmd/mcp-gateway ./cmd/worker-agentd ./cmd/mcxctl
GOCACHE=$${GOCACHE:-$(PWD)/.cache/go-build} go build -buildvcs=false ./cmd/api ./cmd/mcp-gateway ./cmd/worker-agentd ./cmd/mcxctl

frontend-install: ensure-dev-image
MULTICODEX_DEV_IMAGE=$(DEV_IMAGE_TAG) $(COMPOSE_DEV) run --rm dev pnpm --dir apps/web install

frontend-lint: ensure-dev-image
MULTICODEX_DEV_IMAGE=$(DEV_IMAGE_TAG) $(COMPOSE_DEV) run --rm dev bash -lc 'pnpm --dir apps/web install --frozen-lockfile && pnpm --dir apps/web lint'

frontend-test: ensure-dev-image
MULTICODEX_DEV_IMAGE=$(DEV_IMAGE_TAG) $(COMPOSE_DEV) run --rm dev bash -lc 'pnpm --dir apps/web install --frozen-lockfile && pnpm --dir apps/web test'

frontend-build: ensure-dev-image
MULTICODEX_DEV_IMAGE=$(DEV_IMAGE_TAG) $(COMPOSE_DEV) run --rm dev bash -lc 'pnpm --dir apps/web install --frozen-lockfile && pnpm --dir apps/web build'

compose-config:
MULTICODEX_DEV_IMAGE=$(DEV_IMAGE_TAG) $(COMPOSE_DEV) config >/dev/null
POSTGRES_PASSWORD="$${POSTGRES_PASSWORD:-$$(openssl rand -hex 16)}" $(COMPOSE_PROD) config >/dev/null
POSTGRES_PASSWORD="$${POSTGRES_PASSWORD:-$$(openssl rand -hex 16)}" $(COMPOSE) -f deployments/docker/compose.yaml -f deployments/docker/compose.ha.yaml config >/dev/null

migrate-dev:
scripts/migrate-dev.sh
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,9 @@ make migrate-dev
## Documentation

Start with [docs/README.md](docs/README.md). The original technical plan is preserved in [multi-codex_technical_plan.md](multi-codex_technical_plan.md), while implementation notes are split into focused folders under `docs/`.

Current delivery posture:

- MVP and enterprise pilot readiness: complete through M0–M6.
- Next track: internal GA launch plan in [docs/implementation/launch-plan.md](docs/implementation/launch-plan.md).
- Frontend track: mature the console into a workbench via [docs/implementation/frontend-workbench-plan.md](docs/implementation/frontend-workbench-plan.md).
12 changes: 12 additions & 0 deletions apps/web/e2e/smoke.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { test, expect } from "@playwright/test";

test("login route renders workbench sign-in shell", async ({ page }) => {
await page.goto("/login");
await expect(page.getByRole("heading", { name: "multi-codex" }).first()).toBeVisible();
await expect(page.locator("body")).toContainText(/sign in|登录|Connect|连接/i);
});

test("unknown protected path redirects unauthenticated users to login", async ({ page }) => {
await page.goto("/runs");
await expect(page).toHaveURL(/\/login/);
});
25 changes: 25 additions & 0 deletions apps/web/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import js from "@eslint/js";
import prettier from "eslint-config-prettier";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";

export default tseslint.config(
{ ignores: ["dist", "coverage", "playwright-report", "test-results", "node_modules"] },
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ["**/*.{ts,tsx}"],
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh
},
rules: {
...reactHooks.configs.recommended.rules,
"react-hooks/set-state-in-effect": "off",
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }]
}
},
prettier
);
3 changes: 3 additions & 0 deletions apps/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
<title>multi-codex</title>
</head>
<body>
Expand Down
39 changes: 37 additions & 2 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,54 @@
"scripts": {
"dev": "vite --host 0.0.0.0",
"build": "tsc --noEmit && vite build",
"preview": "vite preview --host 0.0.0.0"
"preview": "vite preview --host 0.0.0.0",
"lint": "eslint .",
"test": "vitest run",
"test:e2e": "playwright test",
"format": "prettier --write ."
},
"dependencies": {
"@hookform/resolvers": "^5.4.0",
"@radix-ui/react-dialog": "^1.1.21",
"@radix-ui/react-dropdown-menu": "^2.1.22",
"@radix-ui/react-slot": "^1.3.1",
"@radix-ui/react-tabs": "^1.1.19",
"@radix-ui/react-toast": "^1.2.21",
"@radix-ui/react-tooltip": "^1.2.14",
"@tanstack/react-query": "^5.0.0",
"@tanstack/react-table": "^8.21.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^1.26.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-hook-form": "^7.82.0",
"react-router-dom": "^7.18.1",
"tailwind-merge": "^3.6.0",
"zod": "^4.0.0"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.61.1",
"@tailwindcss/vite": "^4.3.3",
"@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^26.1.1",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitejs/plugin-react": "^5.0.0",
"@vitest/coverage-v8": "^4.1.10",
"eslint": "^10.7.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.3",
"jsdom": "^29.1.1",
"prettier": "^3.9.6",
"tailwindcss": "^4.3.3",
"typescript": "^5.9.0",
"vite": "^8.0.0"
"typescript-eslint": "^8.65.0",
"vite": "^8.0.0",
"vitest": "^4.1.10"
}
}
18 changes: 18 additions & 0 deletions apps/web/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
forbidOnly: Boolean(process.env.CI),
retries: process.env.CI ? 1 : 0,
use: {
baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "http://127.0.0.1:4173",
trace: "on-first-retry"
},
projects: [{ name: "chromium", use: { ...devices["Desktop Chrome"] } }],
webServer: {
command: "pnpm preview --host 127.0.0.1 --port 4173",
port: 4173,
reuseExistingServer: !process.env.CI
}
});
Loading
Loading