Skip to content

Commit b7dc937

Browse files
GenerQAQclaude
andauthored
feat(api): merge Acontext-admin fork into main repo (#458)
* feat(api): merge Acontext-admin fork into main repo Add admin API as a second entry point (cmd/admin/main.go) sharing the same Go module. This eliminates the need for a separate fork and manual sync-api.sh workflow. - Add admin handlers, services, and repos for project management, usage analytics, and metrics - Add SupabaseAuth and MetricsAuth middleware - Add admin bootstrap container extending the base DI container - Add admin router extending the base router with /admin/v1 and /metrics/v1 route groups - Add Dockerfile.admin and CI/CD workflows (admin/vX.Y.Z tags) - Add MetricsCfg, SupabaseCfg, and JaegerQueryEndpoint config fields - Add PathMatcher utility for quota route matching Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(api): address code review issues from admin merge PR #458 - Reuse SupabaseAuth client across requests instead of creating per-request - Wrap storage metrics delete+create in a DB transaction to prevent data loss - Reuse http.Client in projectService instead of creating per-request - Whitelist safe headers (Accept, Content-Type) for Jaeger proxy instead of forwarding all - Remove bearer token plaintext log at admin startup Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(api): address additional code review issues for admin merge PR #458 Remove unused MetricTag constants to align Go with Python, update config.admin.yaml pool defaults to match PR #457, fix Swagger BasePath annotation, whitelist Jaeger proxy query params, and filter unsafe response headers in Jaeger proxy. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(api): fix stale table references and GORM autoUpdateTime in admin merge - Replace removed `spaces` table with `learning_spaces` in AnalyzeUsages - Replace removed `tool_references` table with `agent_skills` in AnalyzeStatistics - Remove autoUpdateTime from Metric model to preserve epoch sentinel values set by quota logic Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent bc630a8 commit b7dc937

21 files changed

Lines changed: 2851 additions & 4 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
name: Admin API Release
2+
3+
on:
4+
push:
5+
tags:
6+
- "admin/v*"
7+
8+
permissions:
9+
contents: write
10+
packages: write
11+
12+
jobs:
13+
build-and-push:
14+
runs-on: ubuntu-latest
15+
timeout-minutes: 60
16+
steps:
17+
- name: Checkout
18+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
19+
with:
20+
fetch-depth: 0
21+
22+
- name: Log in to GitHub Container Registry
23+
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2
24+
with:
25+
registry: ghcr.io
26+
username: ${{ github.repository_owner }}
27+
password: ${{ secrets.GITHUB_TOKEN }}
28+
29+
- name: Extract metadata (tags, labels) for Docker
30+
id: meta
31+
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf
32+
with:
33+
images: ghcr.io/memodb-io/acontext-admin
34+
tags: |
35+
type=sha
36+
type=semver,pattern={{version}},match=admin/v(\d+\.\d+\.\d+)$
37+
38+
- name: Set up QEMU
39+
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a
40+
41+
- name: Set up Docker Buildx
42+
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd
43+
44+
- name: Build and Push Docker image
45+
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294
46+
with:
47+
platforms: linux/amd64,linux/arm64
48+
context: ./src/server/api/go
49+
file: ./src/server/api/go/Dockerfile.admin
50+
push: true
51+
tags: ${{ steps.meta.outputs.tags }}
52+
labels: ${{ steps.meta.outputs.labels }}
53+
sbom: true
54+
provenance: mode=max
55+
cache-from: type=gha,scope=acontext-admin
56+
cache-to: type=gha,mode=max,scope=acontext-admin
57+
58+
- name: Generate Changelog
59+
run: |
60+
bash .github/scripts/generate-changelog.sh \
61+
--tag-prefix "admin/v" \
62+
--source-dir "src/server/api/go" \
63+
--display-name "Admin API" \
64+
--output "$GITHUB_WORKSPACE-CHANGELOG.txt" \
65+
--footer "Published to https://github.com/memodb-io/Acontext/pkgs/container/acontext-admin"
66+
67+
- name: Create Release
68+
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe
69+
with:
70+
body_path: ${{ github.workspace }}-CHANGELOG.txt

.github/workflows/admin-test.yaml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
name: Go Admin API Test
2+
3+
on:
4+
push:
5+
branches:
6+
- dev
7+
paths:
8+
- 'src/server/api/go/**'
9+
- '.github/workflows/admin-test.yaml'
10+
pull_request:
11+
branches:
12+
- dev
13+
paths:
14+
- 'src/server/api/go/**'
15+
- '.github/workflows/admin-test.yaml'
16+
17+
permissions:
18+
contents: read
19+
20+
concurrency:
21+
group: ${{ github.workflow }}-${{ github.ref }}
22+
cancel-in-progress: true
23+
24+
jobs:
25+
test:
26+
runs-on: ubuntu-latest
27+
timeout-minutes: 30
28+
defaults:
29+
run:
30+
working-directory: src/server/api/go
31+
steps:
32+
- name: Checkout
33+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd
34+
- name: Setup Go
35+
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417
36+
with:
37+
go-version-file: src/server/api/go/go.mod
38+
cache: true
39+
cache-dependency-path: src/server/api/go/go.sum
40+
- run: go mod tidy
41+
- name: Run tests with coverage
42+
run: go test -v -timeout 30m -coverprofile=coverage.out -covermode=atomic ./...
43+
- name: Upload coverage artifact
44+
if: always()
45+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f
46+
with:
47+
name: go-admin-coverage
48+
path: src/server/api/go/coverage.out
49+
retention-days: 30

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ When releasing a new version, follow these steps in order:
7777
| Component | Tag Pattern | Publishes To | Source Directory | Workflow |
7878
| ------------------ | ----------------------------------- | ------------------------------------------- | --------------------------------- | ----------------------------------------- |
7979
| API | `api/vX.Y.Z` | ghcr.io (Docker, multi-arch) | `src/server/api/go` | `api-release.yaml` |
80+
| Admin API | `admin/vX.Y.Z` | ghcr.io (Docker, multi-arch) | `src/server/api/go` | `admin-release.yaml` |
8081
| Core | `core/vX.Y.Z` | ghcr.io (Docker, multi-arch) | `src/server/core` | `core-release.yaml` |
8182
| UI (OSS) | `ui/vX.Y.Z` | ghcr.io (Docker, multi-arch) | `src/server/ui` | `ui-release.yaml` |
8283
| TypeScript SDK | `sdk-ts/vX.Y.Z` | npm (`@acontext/acontext`) | `src/client/acontext-ts` | `client-release-ts.yaml` |

src/server/api/go/Dockerfile.admin

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
FROM --platform=$BUILDPLATFORM golang:1.26 AS build-stage
2+
3+
ARG TARGETPLATFORM
4+
ARG BUILDPLATFORM
5+
RUN echo "I am running on $BUILDPLATFORM, building for $TARGETPLATFORM"
6+
7+
WORKDIR /app
8+
9+
ARG TARGETOS
10+
ARG TARGETARCH
11+
12+
RUN --mount=target=. \
13+
--mount=type=cache,target=/root/.cache/go-build \
14+
--mount=type=cache,target=/go/pkg \
15+
GOOS=${TARGETOS} GOARCH=${TARGETARCH} CGO_ENABLED=0 go build -o /acontext-admin ./cmd/admin/main.go
16+
17+
FROM build-stage AS run-test-stage
18+
RUN go test -v ./...
19+
20+
FROM alpine:3.23 AS build-release-stage
21+
22+
WORKDIR /
23+
24+
COPY --from=build-stage /acontext-admin /acontext-admin
25+
COPY ./configs/config.admin.yaml /configs/config.yaml
26+
27+
28+
# TODO: use non-root user
29+
30+
EXPOSE 8028
31+
32+
ENTRYPOINT ["/acontext-admin"]
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package main
2+
3+
// @title Acontext Admin API
4+
// @version 1.0
5+
// @description API for Acontext Admin.
6+
// @schemes http https
7+
// @BasePath /
8+
9+
// Bearer at Project level
10+
// @securityDefinitions.apikey BearerAuth
11+
// @in header
12+
// @name Authorization
13+
// @description Dynamically generate a signature for admin-level authentication
14+
15+
import (
16+
"context"
17+
"fmt"
18+
"net/http"
19+
"os"
20+
"os/signal"
21+
"syscall"
22+
"time"
23+
24+
"github.com/gin-gonic/gin"
25+
"github.com/memodb-io/Acontext/internal/bootstrap"
26+
"github.com/memodb-io/Acontext/internal/config"
27+
"github.com/memodb-io/Acontext/internal/infra/cache"
28+
dbpkg "github.com/memodb-io/Acontext/internal/infra/db"
29+
"github.com/memodb-io/Acontext/internal/modules/handler"
30+
"github.com/memodb-io/Acontext/internal/pkg/tokenizer"
31+
"github.com/memodb-io/Acontext/internal/router"
32+
"github.com/memodb-io/Acontext/internal/telemetry"
33+
"github.com/redis/go-redis/v9"
34+
"github.com/samber/do"
35+
"go.uber.org/zap"
36+
"gorm.io/gorm"
37+
)
38+
39+
func main() {
40+
// build dependency injection container (admin = base + admin deps)
41+
inj := bootstrap.BuildAdminContainer()
42+
43+
cfg := do.MustInvoke[*config.Config](inj)
44+
log := do.MustInvoke[*zap.Logger](inj)
45+
db := do.MustInvoke[*gorm.DB](inj)
46+
rdb := do.MustInvoke[*redis.Client](inj)
47+
48+
// Initialize tokenizer (vocabulary is already embedded in the package)
49+
if err := tokenizer.Init(log); err != nil {
50+
log.Sugar().Fatalw("failed to initialize tokenizer", "err", err)
51+
}
52+
53+
// Setup OpenTelemetry tracing (using configuration system)
54+
tp, err := telemetry.SetupTracing(cfg)
55+
if err != nil {
56+
log.Sugar().Warnw("failed to setup tracing, continuing without tracing", "err", err)
57+
} else if tp != nil {
58+
log.Sugar().Info("OpenTelemetry tracing enabled", "endpoint", cfg.Telemetry.OtlpEndpoint)
59+
defer func() {
60+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
61+
defer cancel()
62+
if err := telemetry.Shutdown(ctx); err != nil {
63+
log.Sugar().Errorw("failed to shutdown tracer", "err", err)
64+
}
65+
}()
66+
67+
// Register GORM OpenTelemetry plugin after tracer provider is set
68+
if err := dbpkg.RegisterOpenTelemetryPlugin(db); err != nil {
69+
log.Sugar().Warnw("failed to register GORM OpenTelemetry plugin, continuing without database tracing", "err", err)
70+
} else {
71+
log.Sugar().Info("GORM OpenTelemetry plugin registered")
72+
}
73+
74+
// Register Redis OpenTelemetry plugin after tracer provider is set
75+
if err := cache.RegisterOpenTelemetryPlugin(rdb); err != nil {
76+
log.Sugar().Warnw("failed to register Redis OpenTelemetry plugin, continuing without Redis tracing", "err", err)
77+
} else {
78+
log.Sugar().Info("Redis OpenTelemetry plugin registered")
79+
}
80+
}
81+
82+
// init gin
83+
gin.SetMode(cfg.App.Env)
84+
85+
// build base handlers
86+
sessionHandler := do.MustInvoke[*handler.SessionHandler](inj)
87+
diskHandler := do.MustInvoke[*handler.DiskHandler](inj)
88+
artifactHandler := do.MustInvoke[*handler.ArtifactHandler](inj)
89+
taskHandler := do.MustInvoke[*handler.TaskHandler](inj)
90+
agentSkillsHandler := do.MustInvoke[*handler.AgentSkillsHandler](inj)
91+
userHandler := do.MustInvoke[*handler.UserHandler](inj)
92+
sandboxHandler := do.MustInvoke[*handler.SandboxHandler](inj)
93+
learningSpaceHandler := do.MustInvoke[*handler.LearningSpaceHandler](inj)
94+
sessionEventHandler := do.MustInvoke[*handler.SessionEventHandler](inj)
95+
projectHandler := do.MustInvoke[*handler.ProjectHandler](inj)
96+
97+
// build admin-specific handlers
98+
adminHandler := do.MustInvoke[*handler.AdminHandler](inj)
99+
metricsHandler := do.MustInvoke[*handler.MetricsHandler](inj)
100+
101+
engine := router.NewAdminRouter(router.AdminRouterDeps{
102+
RouterDeps: router.RouterDeps{
103+
Config: cfg,
104+
DB: db,
105+
Log: log,
106+
SessionHandler: sessionHandler,
107+
DiskHandler: diskHandler,
108+
ArtifactHandler: artifactHandler,
109+
TaskHandler: taskHandler,
110+
AgentSkillsHandler: agentSkillsHandler,
111+
UserHandler: userHandler,
112+
SandboxHandler: sandboxHandler,
113+
LearningSpaceHandler: learningSpaceHandler,
114+
SessionEventHandler: sessionEventHandler,
115+
ProjectHandler: projectHandler,
116+
},
117+
AdminHandler: adminHandler,
118+
MetricsHandler: metricsHandler,
119+
})
120+
121+
addr := fmt.Sprintf("%s:%d", cfg.App.Host, cfg.App.Port)
122+
srv := &http.Server{
123+
Addr: addr,
124+
Handler: engine,
125+
ReadHeaderTimeout: 30 * time.Second,
126+
WriteTimeout: 15 * time.Minute,
127+
IdleTimeout: 120 * time.Second,
128+
}
129+
130+
go func() {
131+
log.Sugar().Infow("starting admin http server", "addr", addr)
132+
log.Sugar().Infow("swagger url", "url", addr+"/swagger/index.html")
133+
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
134+
log.Sugar().Fatalw("listen error", "err", err)
135+
}
136+
}()
137+
138+
// graceful shutdown
139+
quit := make(chan os.Signal, 1)
140+
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
141+
<-quit
142+
143+
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
144+
defer cancel()
145+
if err := srv.Shutdown(ctx); err != nil {
146+
log.Sugar().Errorw("server shutdown", "err", err)
147+
}
148+
log.Sugar().Info("admin server exited")
149+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
app:
2+
name: acontext-admin
3+
env: ${APP_ENV} # available mode: debug / release / test
4+
host: 0.0.0.0
5+
port: ${API_EXPORT_PORT} # Bind to .env 8028
6+
7+
root:
8+
apiBearerToken: "${ROOT_API_BEARER_TOKEN}"
9+
secretPepper: "your-secret-pepper"
10+
enableArgon2Verification: ${ENABLE_ARGON2_VERIFICATION}
11+
12+
log:
13+
level: info # debug/info/warn/error
14+
15+
database:
16+
dsn: "host=${DATABASE_HOST} user=${DATABASE_USER} password=${DATABASE_PASSWORD} dbname=${DATABASE_NAME} port=${DATABASE_EXPORT_PORT} sslmode=disable TimeZone=UTC"
17+
maxOpen: 50
18+
maxIdle: 25
19+
maxIdleTimeSec: 300
20+
autoMigrate: true
21+
enableTLS: ${DATABASE_ENABLE_TLS}
22+
23+
redis:
24+
addr: "${REDIS_HOST}:${REDIS_EXPORT_PORT}"
25+
password: "${REDIS_PASSWORD}"
26+
db: 0
27+
poolSize: 30
28+
enableTLS: ${REDIS_ENABLE_TLS}
29+
30+
rabbitmq:
31+
url: "amqp://${RABBITMQ_USER}:${RABBITMQ_PASSWORD}@${RABBITMQ_HOST}:${RABBITMQ_EXPORT_PORT}/${RABBITMQ_VHOST_ENCODED}"
32+
prefetch: 10
33+
enableTLS: ${RABBITMQ_ENABLE_TLS}
34+
35+
s3:
36+
endpoint: "${S3_ENDPOINT}"
37+
internalEndpoint: "${S3_INTERNAL_ENDPOINT}"
38+
region: "${S3_REGION}"
39+
accessKey: "${S3_ACCESS_KEY}"
40+
secretKey: "${S3_SECRET_KEY}"
41+
bucket: "${S3_BUCKET}"
42+
usePathStyle: true
43+
presignExpireSec: 900
44+
# sse: "aws:kms"
45+
46+
core:
47+
baseURL: "${CORE_BASE_URL}"
48+
49+
metrics:
50+
pushURL: "${METRICS_PUSH_URL}"
51+
52+
telemetry:
53+
otlpEndpoint: "${OTEL_EXPORTER_OTLP_ENDPOINT}"
54+
enabled: true
55+
sampleRatio: 1.0 # Sampling ratio, 0.0-1.0, default 1.0 (100%)
56+
jaegerQueryEndpoint: "${JAEGER_QUERY_ENDPOINT}"
57+
58+
supabase:
59+
projectReference: "${SUPABASE_PROJECT_REFERENCE}"
60+
apiKey: "${SUPABASE_API_KEY}"
61+
authURL: "${SUPABASE_AUTH_URL}" # Optional: custom auth URL
62+
63+
artifact:
64+
maxUploadSizeBytes: ${ARTIFACT_MAX_UPLOAD_SIZE_BYTES} # Default 16MB (16 * 1024 * 1024 bytes)

src/server/api/go/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require (
2121
github.com/samber/do v1.6.0
2222
github.com/spf13/viper v1.21.0
2323
github.com/stretchr/testify v1.11.1
24+
github.com/supabase-community/auth-go v1.5.0
2425
github.com/swaggo/files v1.0.1
2526
github.com/swaggo/gin-swagger v1.6.1
2627
github.com/swaggo/swag v1.16.6
@@ -141,6 +142,7 @@ require (
141142
github.com/tidwall/match v1.2.0 // indirect
142143
github.com/tidwall/pretty v1.2.1 // indirect
143144
github.com/tidwall/sjson v1.2.5 // indirect
145+
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 // indirect
144146
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
145147
github.com/ugorji/go/codec v1.3.1 // indirect
146148
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect

0 commit comments

Comments
 (0)