myxce/.kiro/steering/api-standards.md
Rongsong Shen 13246a89c8
docs: OpenWolf pointer cleanup + redirect-hint checklist item (bug-1700 lesson)
CLAUDE.md: replace the stale @.wolf/OPENWOLF.md import with a pointer to
the openwolf skill / rules file, matching how the project's OpenWolf
setup actually resolves the protocol now.

api-standards.md: add checklist item 6 for new primary-only opcodes --
confirm ha_dispatch_gate's -XCE_ERR_STANDBY rejection actually populates
a redirect via fill_redirect_reply(), not just that the client has a
generic redirect-follower. Directly informed by bug-1700, where the
identical gap was independently rediscovered for OPCode_schedAdmin after
already being documented once for OPCode_upgradeConfig.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AgYRU3zDTqNGV5Jk8aiKLg
2026-08-27 08:27:17 +00:00

6.7 KiB

API Standards

Philosophy

Two API surfaces: Go REST API (apiserver) and C daemon messaging (Cap'n Proto over TCP). Both follow a consistent pattern: validate at the boundary, delegate business logic, return structured responses. Non-blocking I/O throughout the daemon layer.

Go REST API (apiserver)

Handler Pattern

Every resource follows a single consistent pattern:

// {Resource}Controller struct
type JobController struct{}

// {Action}{Resource} handler method
func (j JobController) SubmitJob(ctx *gin.Context) {
    // 1. Validate input at the boundary
    req, err := preCheckSubmitJobRequest(ctx)
    if err != nil {
        ReturnError(ctx, common.INVALID_PARAMS_CODE, err.Error(), nil)
        return
    }

    // 2. Delegate to service layer
    result, err := j.svc.Submit(req)
    if err != nil {
        ReturnError(ctx, common.ERROR_CODE, err.Error(), nil)
        return
    }

    // 3. Return structured success
    ReturnSuccess(ctx, common.SUCCESS_CODE, "submitted", result)
}

Rules:

  • Early return on every error — ReturnError(...); return
  • Keep preCheck* functions in the same file as the handler
  • Controllers delegate to services — business logic never lives in handlers
  • Dependencies flow inward: controllers → services → store/backend

Response Format

Every HTTP response uses a consistent envelope via two helpers in controllers/response.go:

// Success (always HTTP 200)
ReturnSuccess(ctx, code, msg, data)
// → {code: 200, msg: "success", data: {...}}

// Error (HTTP status == code parameter)
ReturnError(ctx, code, msg, details)
// → {code: 401, msg: "access forbidden", details: nil}

Standard Error Codes

All codes defined in common/common.go:

Code Constant Usage
200 SUCCESS_CODE All success responses
400 INVALID_PARAMS_CODE Malformed/missing input
401 ERROR_AUTH_CHECK_TOKEN_CODE Missing/invalid JWT
403 ERROR_AUTH_CODE Valid JWT, insufficient permission
404 ERROR_NOT_FOUND_CODE Resource not found
429 ERROR_TOO_MANY_REQUESTS_CODE Rate limit exceeded
500 ERROR_CODE Unexpected server error

Middleware Chain

Order is defined in router/routers.go and is non-negotiable:

WhitelistURL → WhitelistIP → RateLimit → ValidateHandler (JWT) → CORS → RBAC
  • WhitelistURL: filter by allowed paths (applied first for efficiency)
  • WhitelistIP: abort 403 if client IP not in whitelist
  • RateLimit: 1000 req/sec, burst 2000 (global config, config/app.yaml)
  • ValidateHandler: parse and validate JWT from Authorization: Bearer header; validate body content-type
  • CORS: origin checks via config
  • RBAC: RequirePermission("action:scope") per-route, enforced by controllers/rbac_check.go

Permission Format

"action:scope" — scopes: all, self, cluster, hostgroup, self_ug, self_hg. Defined in config/rbac.yaml. Roles support inheritance via includes:.

C Daemon Messaging

Cap'n Proto Wire Format

All inter-daemon messages are Cap'n Proto serialized. Schema: src/schema/controller.capnp.

Request (xce_request_t):

  • opcode — OPCode enum (43 values)
  • seq — request sequence number
  • from — sender peer info (host/key)
  • body — union of 29 message types (including the empty nomsg placeholder)

Reply (xce_reply_t):

  • opcode — echoes the originating request's opcode by default (init_reply_by_req() sets reply->opcode = req->opcode); many call sites override it with a type-specific opcode (e.g. OPCode_preVoteReply, OPCode_walAck, OPCode_schedAdmin). Consumers dispatch on this value, so it is not a fixed OPCode_reply/OPCode_redirect pair — that pairing only applies to the separate wrapper xce_request_t that xce_reply() builds internally.
  • status — error code (0 = success)
  • seq — echoes request seq
  • data — union of reply payloads (clusterconf, jobid, stateReport, walackreplymsg, etc.)

Key OPCodes

Group OPCodes Purpose
Control jobsub (11), jobdispatch (8), jobcontrol (15) Job lifecycle
Cluster hello (2), join (3), clusterconf (17), heartbeat (25) Membership and liveness
Replication replConfig (29), replConfigPush (30), walCatchupEntry (37), snapshotTransfer (36) Config + WAL replication
Vote requestVote, reply Raft election
Data push (6), get (5), notify (10) Data exchange

Daemon I/O Functions

// Enqueue request to endpoint send buffer (non-blocking, always)
int xce_request(endpoint_t *ep, xce_request_t *req);

// Enqueue reply to inbound request sender (non-blocking)
int xce_reply(endpoint_t *ep, xce_peerinfo_t *host, xce_reply_t *reply);

// Cap'n Proto serialize request to packet
int xce_encode(xce_request_t *req, io_packet_t *pkt);

// Cap'n Proto deserialize packet to request
int xce_decode(io_packet_t *pkt, xce_request_t *req);

msg_handler callback: Each endpoint has ep->msg_handler — a function pointer int (*)(void*) called by iomgr when a packet arrives. Handlers must return quickly (vtask context). Registered per-endpoint during connection setup.

Adding a New Opcode

  1. Add the opcode constant to the OPCode enum in src/schema/controller.capnp
  2. Add the message struct to src/schema/controller.capnp (Request/Reply union arms)
  3. Rebuild capnp schemas (automatic via meson)
  4. Add a case OPCode_...: in the switch (request.opcode) dispatch inside myreq_handler() in cluster.c (this runs after ha_dispatch_gate, which only applies a role-based allow/deny filter, not per-opcode handler logic)
  5. Add encode/decode paths in src/lib/request.c if using raw payloads
  6. If the new opcode is primary-only (absent from ha_dispatch_gate's CTRL_STANDBY allow-list), confirm its rejection reply reaches a client that can actually recover — myreq_handler's generic gate-rejection path (cluster.c, right after the ha_dispatch_gate() call) must populate the redirect fields via fill_redirect_reply() (join.c) when rejecting for -XCE_ERR_STANDBY, mirroring OPCode_join's handler. Every client-side redirect-follower in this codebase (xceadmin.discovery.follow_redirects(), libxce's own failover path) is already generic/opcode-agnostic and works automatically the moment the server supplies a redirect — don't assume "the client should just retry" is a client-side gap without first checking whether the server ever gave it anything to retry against (bug-1700: the identical gap was independently found and documented for OPCode_upgradeConfig first — test_e2e_live_ha_push.py's module docstring — then shipped again under OPCode_schedAdmin before either was actually fixed).