Files
probo/contrib/claude/api-surface.md
Ludovic Vielle e767dd8377 Add device enrollment API and agent protocol
Expose ITAM REST endpoints for agents, console GraphQL for device
management, and wire probod bootstrap with enrollment e2e coverage.

Signed-off-by: Ludovic Vielle <ludovic@probo.com>
2026-07-24 15:08:23 +02:00

2.9 KiB

API Surface Rules

Every feature must be exposed through all four interfaces: GraphQL, MCP, CLI, and n8n. When adding a new endpoint or editing an existing type, keep all four in sync:

If you add a mutation in GraphQL, add the corresponding MCP tool, CLI command, and n8n node. If you rename or change a type, update it everywhere.

Every new Go API endpoint must have end-to-end tests in e2e/.

Error handling — never leak internal details

By default every error returned to the end user must be an opaque internal error. Only errors that are explicitly matched and mapped to a known category may surface a meaningful message. Unrecognized or unexpected errors are always replaced with a generic "internal server error" response — never expose stack traces, SQL errors, file paths, or any implementation detail.

Allowed user-facing error categories

Category GraphQL helper HTTP helper When to use
Not found gqlutils.NotFound / NotFoundf jsonx.RenderNotFound Resource does not exist or is not visible to the caller
Forbidden gqlutils.Forbidden / Forbiddenf jsonx.RenderForbidden Caller lacks permission (after authentication)
Invalid gqlutils.Invalid / Invalidf / InvalidValidationErrors jsonx.RenderBadRequest Validation failure on user-supplied input
Conflict gqlutils.Conflict / Conflictf — Unique constraint or state conflict
Unauthenticated gqlutils.Unauthenticated / Unauthenticatedf jsonx.RenderUnauthorized Missing or expired credentials

Catch-all is always internal

Any error that does not match one of the categories above must be returned as:

  • GraphQL — gqlutils.Internal(ctx) (fixed generic message, no error details)
  • HTTP — jsonx.RenderInternalServerError(w) (fixed 500 body, no error details)
  • MCP — return a generic "internal error" string; never forward err.Error()

Log the original error server-side (with request/trace IDs) so it can be investigated, but never include it in the response.

Pattern in resolvers

result, err := s.doSomething(ctx, req)
if err != nil {
    switch {
    case errors.Is(err, probo.ErrNotFound):
        return nil, gqlutils.NotFoundf(ctx, "thing %q not found", id)
    case errors.Is(err, probo.ErrConflict):
        return nil, gqlutils.Conflictf(ctx, "thing already exists")
    default:
        logger.ErrorCtx(ctx, "cannot do something", log.Error(err))
        return nil, gqlutils.Internal(ctx)
    }
}

The default branch must always be present and must always return the generic internal error.