Update agent rules

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2026-04-19 11:49:55 +02:00
parent 29818714a8
commit 5efe9e5b0f
2 changed files with 68 additions and 0 deletions

View File

@@ -10,3 +10,46 @@ Every feature must be exposed through **all four interfaces**: GraphQL, MCP, CLI
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` | `jsonutil.RenderNotFound` | Resource does not exist or is not visible to the caller |
| Forbidden | `gqlutils.Forbidden` / `Forbiddenf` | `jsonutil.RenderForbidden` | Caller lacks permission (after authentication) |
| Invalid | `gqlutils.Invalid` / `Invalidf` / `InvalidValidationErrors` | `jsonutil.RenderBadRequest` | Validation failure on user-supplied input |
| Conflict | `gqlutils.Conflict` / `Conflictf` | — | Unique constraint or state conflict |
| Unauthenticated | `gqlutils.Unauthenticated` / `Unauthenticatedf` | — | 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** — `jsonutil.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
```go
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.