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
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: Bearerheader; validate body content-type - CORS: origin checks via config
- RBAC:
RequirePermission("action:scope")per-route, enforced bycontrollers/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 numberfrom— sender peer info (host/key)body— union of 29 message types (including the emptynomsgplaceholder)
Reply (xce_reply_t):
opcode— echoes the originating request's opcode by default (init_reply_by_req()setsreply->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 fixedOPCode_reply/OPCode_redirectpair — that pairing only applies to the separate wrapperxce_request_tthatxce_reply()builds internally.status— error code (0 = success)seq— echoes request seqdata— 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
- Add the opcode constant to the
OPCodeenum insrc/schema/controller.capnp - Add the message struct to
src/schema/controller.capnp(Request/Reply union arms) - Rebuild capnp schemas (automatic via meson)
- Add a
case OPCode_...:in theswitch (request.opcode)dispatch insidemyreq_handler()incluster.c(this runs afterha_dispatch_gate, which only applies a role-based allow/deny filter, not per-opcode handler logic) - Add encode/decode paths in
src/lib/request.cif using raw payloads - If the new opcode is primary-only (absent from
ha_dispatch_gate'sCTRL_STANDBYallow-list), confirm its rejection reply reaches a client that can actually recover —myreq_handler's generic gate-rejection path (cluster.c, right after theha_dispatch_gate()call) must populate the redirect fields viafill_redirect_reply()(join.c) when rejecting for-XCE_ERR_STANDBY, mirroringOPCode_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 forOPCode_upgradeConfigfirst —test_e2e_live_ha_push.py's module docstring — then shipped again underOPCode_schedAdminbefore either was actually fixed).